generated from jric11/baseProject
Codex's updates...
This commit is contained in:
+210
-218
@@ -3,9 +3,13 @@
|
||||
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). Thia is always true of the master branch. Some earlier branches remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version.
|
||||
and this project adheres to [Semantic Versioning](https://semver.org). This is always true of the master branch.
|
||||
Dropping support for EOL versions of PHP will not be considered
|
||||
a breaking change.
|
||||
|
||||
## TBD - 5.3.0
|
||||
Some earlier branches remain supported and security fixes are applied to them; if the security fix represents a breaking change, it may have to be applied as a minor or patch version.
|
||||
|
||||
## TBD - 5.11.0
|
||||
|
||||
### Added
|
||||
|
||||
@@ -31,6 +35,210 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
## 2026-09-15 - 5.10.0
|
||||
|
||||
### Added
|
||||
|
||||
- XLSX password-to-open encryption and decryption using Office Agile encryption. [Issue #3878](https://github.com/PHPOffice/PhpSpreadsheet/issues/3878) [PR #4975](https://github.com/PHPOffice/PhpSpreadsheet/pull/4975) [PR #4988](https://github.com/PHPOffice/PhpSpreadsheet/pull/4988)
|
||||
- Support for Excel sparklines (line, column, and win/loss) in Xlsx reader and writer. [Issue #4941](https://github.com/PHPOffice/PhpSpreadsheet/issues/4941)
|
||||
- Read-only object model for Pivot Tables. Existing pivot tables in an Xlsx file are now parsed into `Worksheet\PivotTable\PivotTable` objects (name, location, source cache definition, and row/column/page/data field layout), accessible via `Worksheet::getPivotTableCollection()` / `getPivotTableByName()`. Pivot tables (their tables, caches and records) are now also preserved through an Xlsx load/save round-trip instead of being silently dropped. [Issue #4534](https://github.com/PHPOffice/PhpSpreadsheet/issues/4534)
|
||||
|
||||
### Changed
|
||||
|
||||
- Performance: avoid `Worksheet::getStyle()` on every `Cell::setValueExplicit()` unless quote-prefix must change. On a dense 40k-cell populate+save microbenchmark this cut wall time by ~5%. [PR #4958](https://github.com/PHPOffice/PhpSpreadsheet/pull/4958)
|
||||
- Infrastructure changes for documentation. [PR #4973](https://github.com/PHPOffice/PhpSpreadsheet/pull/4973)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Security patches.
|
||||
- Xls Writer now creates conformant CFB/DIFAT metadata at FAT-sector boundaries. [Issue #4811](https://github.com/PHPOffice/PhpSpreadsheet/issues/4811) [PR #4983](https://github.com/PHPOffice/PhpSpreadsheet/pull/4983)
|
||||
- Ods Reader/Writer Drawings. [Issue #4809](https://github.com/PHPOffice/PhpSpreadsheet/issues/4809) [PR #4956](https://github.com/PHPOffice/PhpSpreadsheet/pull/4956)
|
||||
- Ods Writer no longer converts cell references or commas inside string literals, so `="THIS IS E1"` is written unchanged instead of as `="THIS IS [.E1]"`. [Issue #4454](https://github.com/PHPOffice/PhpSpreadsheet/issues/4454) [PR #4962](https://github.com/PHPOffice/PhpSpreadsheet/pull/4962)
|
||||
- Ods Reader/Writer slight improvement for Date styles. [PR #4960](https://github.com/PHPOffice/PhpSpreadsheet/pull/4960)
|
||||
- BETAINV/BETA.INV no longer abandons its search when the Beta CDF underflows to zero (wrong results for alpha above about 1080). [PR #4954](https://github.com/PHPOffice/PhpSpreadsheet/pull/4954)
|
||||
- GAMMA.INV, the GAMMA.DIST/CHISQ.DIST/F.DIST densities, and GAMMALN no longer fail or return wrong results for large shape parameters / degrees of freedom. [PR #4953](https://github.com/PHPOffice/PhpSpreadsheet/pull/4953)
|
||||
- GAMMAINV/GAMMA.INV no longer clamps upper-tail quantiles beyond alpha*beta*5. [PR #4946](https://github.com/PHPOffice/PhpSpreadsheet/pull/4946)
|
||||
- Correct incomplete gamma convergence for GAMMA.DIST/CHISQ.DIST family (wrong once the series argument reached ~32). [PR #4945](https://github.com/PHPOffice/PhpSpreadsheet/pull/4945)
|
||||
- Fix problem with VLOOKUP and whole-column ranges. [Issue #4969](https://github.com/PHPOffice/PhpSpreadsheet/issues/4969) [PR #4967](https://github.com/PHPOffice/PhpSpreadsheet/pull/4967)
|
||||
- Problem with Xlsx Writer and header-row table. [PR #4968](https://github.com/PHPOffice/PhpSpreadsheet/pull/4968)
|
||||
- Throw when Ods Reader encounters invalid Xml. [PR #4986](https://github.com/PHPOffice/PhpSpreadsheet/pull/4986)
|
||||
- Update Sheetname in Charts when Sheetname changes. [Issue #744](https://github.com/PHPOffice/PhpSpreadsheet/issues/744) [PR #4984](https://github.com/PHPOffice/PhpSpreadsheet/pull/4984)
|
||||
|
||||
## 2026-07-12 - 5.9.0
|
||||
|
||||
### Removed
|
||||
|
||||
- **Drop support for PHP 8.1**, according to [our published guidelines](https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support). [PR #4907](https://github.com/PHPOffice/PhpSpreadsheet/pull/4907)
|
||||
|
||||
### Added
|
||||
|
||||
- Navigate Cell as Cursor. [Issue #863](https://github.com/PHPOffice/PhpSpreadsheet/issues/863) [PR #4909](https://github.com/PHPOffice/PhpSpreadsheet/pull/4909)
|
||||
- Chart DataTables. [Issue #413](https://github.com/PHPOffice/PhpSpreadsheet/issues/413) [PR #4911](https://github.com/PHPOffice/PhpSpreadsheet/pull/4911)
|
||||
- Permit Ignoring "Misleading Format" Tag. [PR #4914](https://github.com/PHPOffice/PhpSpreadsheet/pull/4914)
|
||||
|
||||
### Changed
|
||||
|
||||
- Restructure parsing logic in Reader/Xlsx. [PR #4830](https://github.com/PHPOffice/PhpSpreadsheet/pull/4830)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Small improvement for Radar Charts. [Issue #661](https://github.com/PHPOffice/PhpSpreadsheet/issues/661) [PR #4908](https://github.com/PHPOffice/PhpSpreadsheet/pull/4908)
|
||||
- Allow Use of Multiple Ranges When Setting Styles. [Issue #411](https://github.com/PHPOffice/PhpSpreadsheet/issues/411) [PR #4910](https://github.com/PHPOffice/PhpSpreadsheet/pull/4910)
|
||||
- Small change to Writer Html. [Issue #434](https://github.com/PHPOffice/PhpSpreadsheet/issues/434) [PR #4912](https://github.com/PHPOffice/PhpSpreadsheet/pull/4912)
|
||||
- Avoid hard-coding some chart Xml attributes. [PR #4915](https://github.com/PHPOffice/PhpSpreadsheet/pull/4915)
|
||||
- Readers should directly access readFilter. [PR #4919](https://github.com/PHPOffice/PhpSpreadsheet/pull/4919)
|
||||
- Very minor changes to Worksheet, Reader/Xlsx, and Worksheet/AutoFilter. [Issue #4917](https://github.com/PHPOffice/PhpSpreadsheet/issues/4917) [PR #4926](https://github.com/PHPOffice/PhpSpreadsheet/pull/4926)
|
||||
- Consistent HighestRow/Column after row/columnDelete. [Issue #943](https://github.com/PHPOffice/PhpSpreadsheet/issues/943) [PR #4925](https://github.com/PHPOffice/PhpSpreadsheet/pull/4925)
|
||||
|
||||
## 2026-06-06 - 5.8.0
|
||||
|
||||
### Security Note
|
||||
|
||||
- File::prohibitWrappers and Drawing::setPath now reject phar paths with extra leading slashes (e.g. phar:///…) that escaped the prior parse_url-based filter. No security exploit was possible even with the extra slashes. [PR #4876](https://github.com/PHPOffice/PhpSpreadsheet/pull/4876)
|
||||
|
||||
### Added
|
||||
|
||||
- Optional method to increase Calculation Engine's parsing speed. [PR #4829](https://github.com/PHPOffice/PhpSpreadsheet/pull/4829)
|
||||
- Html Writer/Reader new data attributes. [PR #4858](https://github.com/PHPOffice/PhpSpreadsheet/pull/4858)
|
||||
- IReader2 interface extending IReader with listWorksheetInfo/Names. [Issue #4883](https://github.com/PHPOffice/PhpSpreadsheet/issues/4883) [PR #4886](https://github.com/PHPOffice/PhpSpreadsheet/pull/4886)
|
||||
|
||||
### Changed
|
||||
|
||||
- Helper/Handler is now internal and final. [PR #4896](https://github.com/PHPOffice/PhpSpreadsheet/pull/4896)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Html Writer handle text colors a bit better. [PR #4855](https://github.com/PHPOffice/PhpSpreadsheet/pull/4855)
|
||||
- Html Writer apply rotation to images. [Issue #4875](https://github.com/PHPOffice/PhpSpreadsheet/issues/4875) [PR #4877](https://github.com/PHPOffice/PhpSpreadsheet/pull/4877)
|
||||
- Work around Php8.6 deprecation in mpdf/mpdf. [PR #4878](https://github.com/PHPOffice/PhpSpreadsheet/pull/4878)
|
||||
- Ability to reuse disconnected spreadsheet. [PR #4880](https://github.com/PHPOffice/PhpSpreadsheet/pull/4880)
|
||||
- Allow int parameter for getChartIndex. [PR #4896](https://github.com/PHPOffice/PhpSpreadsheet/pull/4896)
|
||||
|
||||
## 2026-04-19 - 5.7.0
|
||||
|
||||
### Fixed
|
||||
|
||||
- Security patches.
|
||||
|
||||
## 2026-04-09 - 5.6.0
|
||||
|
||||
### Added
|
||||
|
||||
- Make Reader/Csv Extendable, add new preferred Reader/CsvNoEscape class. [Issue #4836](https://github.com/PHPOffice/PhpSpreadsheet/issues/4836) [PR #4837](https://github.com/PHPOffice/PhpSpreadsheet/pull/4837) [PR #4845](https://github.com/PHPOffice/PhpSpreadsheet/pull/4845)
|
||||
- XLOOKUP function. [Issue #1453](https://github.com/PHPOffice/PhpSpreadsheet/issues/1453) [PR #4844](https://github.com/PHPOffice/PhpSpreadsheet/pull/4844)
|
||||
- Introduction of a benchmark test suite, independent of the default unit test suite. Users can use it as a template for experimenting and making decisions concerning performance. [PR #4824](https://github.com/PHPOffice/PhpSpreadsheet/pull/4824)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Collection/Cells::MAX_COLUMN_ID - use Cell/AddressRange::MAX_COLUMN_INT.
|
||||
- Writer/Xls/Worksheet constants MAX_XLS_COLUMN, MAX_XLS_COLUMN_STRING, MAX_XLS_ROW - use Cell/AddressRange MAX_COLUMN_INT_XLS, MAX_COLUMN_XLS, MAX_ROW_XLS
|
||||
|
||||
### Fixed
|
||||
|
||||
- Consistent handling of row and column limits. [PR #4820](https://github.com/PHPOffice/PhpSpreadsheet/pull/4820)
|
||||
- Problems with Html Conditional Formatting Colorscale. [Issue #4838](https://github.com/PHPOffice/PhpSpreadsheet/issues/4838) [PR #4839](https://github.com/PHPOffice/PhpSpreadsheet/pull/4839)
|
||||
- Fixed an issue where Date detection could misclassify invalid numeric values as dates. [PR #4841](https://github.com/PHPOffice/PhpSpreadsheet/pull/4841)
|
||||
- Ods Reader/Writer Integer Styles with Leading Zeros. [Issue #1606](https://github.com/PHPOffice/PhpSpreadsheet/issues/1606) [PR #4822](https://github.com/PHPOffice/PhpSpreadsheet/pull/4822)
|
||||
- Xlsx Writer Data URI for Images. [Issue #4823](https://github.com/PHPOffice/PhpSpreadsheet/issues/4823) [PR #4831](https://github.com/PHPOffice/PhpSpreadsheet/pull/4831)
|
||||
- Confusion Checking for Union Arguments. [Issue #4832](https://github.com/PHPOffice/PhpSpreadsheet/issues/4832) [PR #4835](https://github.com/PHPOffice/PhpSpreadsheet/pull/4835)
|
||||
- Reader Xlsx Hyperlink with Anchor. [Issue #4842](https://github.com/PHPOffice/PhpSpreadsheet/issues/4842) [PR #4843](https://github.com/PHPOffice/PhpSpreadsheet/pull/4843)
|
||||
|
||||
## 2026-02-28 - 5.5.0
|
||||
|
||||
### Added
|
||||
|
||||
- Much improved handling of Styles by Ods Reader. The relevant changes are listed at the end of the "Fixed" section below.
|
||||
- Option to use OldCalculatedValue in ToArray and Relatives. [Issue #1810](https://github.com/PHPOffice/PhpSpreadsheet/issues/1810) [PR #4787](https://github.com/PHPOffice/PhpSpreadsheet/pull/4787)
|
||||
- Add checkbox style (Xlsx and Html). [PR #4781](https://github.com/PHPOffice/PhpSpreadsheet/pull/4781)
|
||||
- Option to whitelist external images. [PR #4793](https://github.com/PHPOffice/PhpSpreadsheet/pull/4793)
|
||||
- Writer/Html add ability to set line ending. [PR #4779](https://github.com/PHPOffice/PhpSpreadsheet/pull/4779)
|
||||
- Writer/Html optionally save formulas as data attributes. [PR #4783](https://github.com/PHPOffice/PhpSpreadsheet/pull/4783)
|
||||
- User-supplied headers and footers in PDF. [Issue #3159](https://github.com/PHPOffice/PhpSpreadsheet/issues/3159) [PR #4789](https://github.com/PHPOffice/PhpSpreadsheet/pull/4789)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Writer/Html constant BODY_LINE no longer makes sense with a configurable line ending. No replacement.
|
||||
- Calculation classes FormulaParser and FormulaToken are unused. No replacement.
|
||||
- Writer/Xls/Worksheet methods insertBitMap, positionImage, writeObjPicture, processBitmapGd, and processBitmap are unused. No replacement.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve performance in value binders. [PR #4780](https://github.com/PHPOffice/PhpSpreadsheet/pull/4780)
|
||||
- Handle Unions as Function Arguments. [Issue #4656](https://github.com/PHPOffice/PhpSpreadsheet/issues/4656) [Issue #316](https://github.com/PHPOffice/PhpSpreadsheet/issues/316) [Issue #503](https://github.com/PHPOffice/PhpSpreadsheet/issues/503) [PR #4657](https://github.com/PHPOffice/PhpSpreadsheet/pull/4657)
|
||||
- Unexpected Behavior of CONCATENATE. [Issue #4061](https://github.com/PHPOffice/PhpSpreadsheet/issues/4061) [PR #4797](https://github.com/PHPOffice/PhpSpreadsheet/pull/4797)
|
||||
- Image Css size in millimeters. [Issue #4800](https://github.com/PHPOffice/PhpSpreadsheet/issues/4800) [PR #4801](https://github.com/PHPOffice/PhpSpreadsheet/pull/4801)
|
||||
- Ods Reader column misalignment. [Issue #4802](https://github.com/PHPOffice/PhpSpreadsheet/issues/4802) [PR #4803](https://github.com/PHPOffice/PhpSpreadsheet/pull/4803)
|
||||
- Ods improved handling of number formats. [Issue #3961](https://github.com/PHPOffice/PhpSpreadsheet/issues/3961) [Issue #4798](https://github.com/PHPOffice/PhpSpreadsheet/issues/4798) [PR #4806](https://github.com/PHPOffice/PhpSpreadsheet/pull/4806)
|
||||
- Ods Reader fonts and fills. [Issue #2622](https://github.com/PHPOffice/PhpSpreadsheet/issues/2622) [Issue #1191](https://github.com/PHPOffice/PhpSpreadsheet/issues/1191) [PR #4810](https://github.com/PHPOffice/PhpSpreadsheet/pull/4810)
|
||||
- Ods Reader alignment and cell protection. [PR #4813](https://github.com/PHPOffice/PhpSpreadsheet/pull/4813)
|
||||
- Ods Reader borders. [PR #4814](https://github.com/PHPOffice/PhpSpreadsheet/pull/4814)
|
||||
- Ods Reader column styles (partial). [PR #4815](https://github.com/PHPOffice/PhpSpreadsheet/pull/4815)
|
||||
|
||||
## 2026-01-10 - 5.4.0
|
||||
|
||||
### Added
|
||||
|
||||
- Store image in cell (Xlsx only). [Issue #4014](https://github.com/PHPOffice/PhpSpreadsheet/issues/4014) [Issue #4034](https://github.com/PHPOffice/PhpSpreadsheet/issues/4034) [Issue #913](https://github.com/PHPOffice/PhpSpreadsheet/issues/913) [PR #4677](https://github.com/PHPOffice/PhpSpreadsheet/pull/4677)
|
||||
- Passthrough support (with some restrictions) for otherwise unsupported drawing elements. [Issue #4037](https://github.com/PHPOffice/PhpSpreadsheet/issues/4037) [Issue #4704](https://github.com/PHPOffice/PhpSpreadsheet/issues/4704) [PR #4712](https://github.com/PHPOffice/PhpSpreadsheet/pull/4712)
|
||||
- Set all locale variables at once in a threadsafe manner. [Issue #954](https://github.com/PHPOffice/PhpSpreadsheet/issues/954) [PR #4760](https://github.com/PHPOffice/PhpSpreadsheet/pull/4760)
|
||||
- Reader/Html add ability to suppress warning messages from loadhtml. [Issue #647](https://github.com/PHPOffice/PhpSpreadsheet/issues/647) [Issue #849](https://github.com/PHPOffice/PhpSpreadsheet/issues/849) [PR #4761](https://github.com/PHPOffice/PhpSpreadsheet/pull/4761)
|
||||
|
||||
### Changed
|
||||
|
||||
- Evaluation of WEBSERVICE no longer requires external client, but will use oldCalculatedValue unless the request is for a domain in a user-supplied whitelist. [PR #4751](https://github.com/PHPOffice/PhpSpreadsheet/pull/4751)
|
||||
|
||||
### Moved
|
||||
|
||||
- Code to merge cell base style with table and conditional styles moved from Html Writer to its own class. [Issue #1058](https://github.com/PHPOffice/PhpSpreadsheet/issues/1058) [PR #4763](https://github.com/PHPOffice/PhpSpreadsheet/pull/4763)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Settings methods setHttpClient, unsetHttpClient, getHttpClient, and getRequestFactory are no longer used. No replacement.
|
||||
- Reader/Html protected property dataArray, described as used only for testing, is not used for testing. No replacement.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Slightly better support for escaped characters in Xlsx Reader/Writer. [Discussion #4724](https://github.com/PHPOffice/PhpSpreadsheet/discussions/4724) [PR #4726](https://github.com/PHPOffice/PhpSpreadsheet/pull/4726)
|
||||
- CODE/UNICODE and CHAR/UNICHAR. [PR #4727](https://github.com/PHPOffice/PhpSpreadsheet/pull/4727)
|
||||
- Minor changes to TextGrid. [PR #4735](https://github.com/PHPOffice/PhpSpreadsheet/pull/4735) [PR #4743](https://github.com/PHPOffice/PhpSpreadsheet/pull/4743)
|
||||
- Single-character table names. [Issue #4739](https://github.com/PHPOffice/PhpSpreadsheet/issues/4739) [PR #4740](https://github.com/PHPOffice/PhpSpreadsheet/pull/4740)
|
||||
- Improvements to SORT and SORTBY. [PR #4743](https://github.com/PHPOffice/PhpSpreadsheet/pull/4743)
|
||||
- Coverage-related changes in Shared. [PR #4745](https://github.com/PHPOffice/PhpSpreadsheet/pull/4745)
|
||||
- ListWorksheetInfo improvements for Xlsx and Ods. [Issue #3255](https://github.com/PHPOffice/PhpSpreadsheet/issues/3255) [PR #4746](https://github.com/PHPOffice/PhpSpreadsheet/pull/4746)
|
||||
- Fix functions related to Student-T distribution. [Issue #4167](https://github.com/PHPOffice/PhpSpreadsheet/issues/4167) [PR #4748](https://github.com/PHPOffice/PhpSpreadsheet/pull/4748)
|
||||
- Fix drawing hyperlinks. [Issue #993](https://github.com/PHPOffice/PhpSpreadsheet/issues/993) [PR #4764](https://github.com/PHPOffice/PhpSpreadsheet/pull/4764)
|
||||
- Fix clone spreadsheet with defined names. [PR #4753](https://github.com/PHPOffice/PhpSpreadsheet/pull/4753)
|
||||
- Changes to WEBSERVICE. [PR #4751](https://github.com/PHPOffice/PhpSpreadsheet/pull/4751)
|
||||
- More consistent handling of unsupported functions. [Issue #606](https://github.com/PHPOffice/PhpSpreadsheet/issues/606) [PR #4772](https://github.com/PHPOffice/PhpSpreadsheet/pull/4772)
|
||||
- Chart shadow `kx` and `ky`. [PR #4770](https://github.com/PHPOffice/PhpSpreadsheet/pull/4770)
|
||||
- SUBTOTAL and hidden rows. [Issue #820](https://github.com/PHPOffice/PhpSpreadsheet/issues/820) [PR #4765](https://github.com/PHPOffice/PhpSpreadsheet/pull/4765)
|
||||
- Fix some hyperlink problems. [Issue #3889](https://github.com/PHPOffice/PhpSpreadsheet/issues/3889) [Issue #2464](https://github.com/PHPOffice/PhpSpreadsheet/issues/2464) [PR #4771](https://github.com/PHPOffice/PhpSpreadsheet/pull/4771)
|
||||
- Xls Writer and empty RichText. [Issue #918](https://github.com/PHPOffice/PhpSpreadsheet/issues/918) [PR #4769](https://github.com/PHPOffice/PhpSpreadsheet/pull/4769)
|
||||
- Strings that look like huge floating-point numbers. [Issue #4766](https://github.com/PHPOffice/PhpSpreadsheet/issues/4766) [PR #4768](https://github.com/PHPOffice/PhpSpreadsheet/pull/4768)
|
||||
- Rowspan in Html. [Issue #1319](https://github.com/PHPOffice/PhpSpreadsheet/issues/1319) [PR #4767](https://github.com/PHPOffice/PhpSpreadsheet/pull/4767)
|
||||
- Mpdf styling of multi-line strings. [Issue #4773](https://github.com/PHPOffice/PhpSpreadsheet/issues/4773) [PR #4775](https://github.com/PHPOffice/PhpSpreadsheet/pull/4775)
|
||||
|
||||
## 2025-11-24 - 5.3.0
|
||||
|
||||
### Added
|
||||
|
||||
- Formal support for Php8.5.
|
||||
- Limited Printarea support for Html/Pdf. [Issue #3941](https://github.com/PHPOffice/PhpSpreadsheet/issues/3941) [PR #4711](https://github.com/PHPOffice/PhpSpreadsheet/pull/4711)
|
||||
- Implement missing `INFO` function. [PR #4709](https://github.com/PHPOffice/PhpSpreadsheet/pull/4709)
|
||||
- Implement missing `BAHTTEXT` function. [PR #4715](https://github.com/PHPOffice/PhpSpreadsheet/pull/4715)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- $dataType, the second parameter of Cell::setValueExplicit, is currently optional. Omitting it is deprecated, and it will be required in a future release.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Protected ranges and insert/delete rows/columns. [Issue #4695](https://github.com/PHPOffice/PhpSpreadsheet/issues/4695) [PR #4702](https://github.com/PHPOffice/PhpSpreadsheet/pull/4702)
|
||||
- Unexpected Exception in Php DateTime. [Issue #4696](https://github.com/PHPOffice/PhpSpreadsheet/issues/4696) [Issue #917](https://github.com/PHPOffice/PhpSpreadsheet/issues/917) [PR #4697](https://github.com/PHPOffice/PhpSpreadsheet/pull/4697)
|
||||
- Add missing Dutch translation to translation file. [PR #4707](https://github.com/PHPOffice/PhpSpreadsheet/pull/4707)
|
||||
- Fix lots of typos throughout codebase. [PR #4705](https://github.com/PHPOffice/PhpSpreadsheet/pull/4705)
|
||||
- Apply small code style improvements. [PR #4708](https://github.com/PHPOffice/PhpSpreadsheet/pull/4708)
|
||||
|
||||
## 2025-10-25 - 5.2.0
|
||||
|
||||
### Added
|
||||
@@ -250,18 +458,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Worksheet::getProtectedCells - use getProtectedCellRanges instead.
|
||||
- Writer/Html::isMpdf property - use instanceof Mpdf instead.
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Moved
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Xls writer Parser Mishandling True/False Argument. [Issue #4331](https://github.com/PHPOffice/PhpSpreadsheet/issues/4331) [PR #4333](https://github.com/PHPOffice/PhpSpreadsheet/pull/4333)
|
||||
@@ -314,22 +510,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
## 2024-12-08 - 3.6.0
|
||||
|
||||
### Added
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Moved
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Html Reader/Writer Better Handling of Booleans. [PR #4257](https://github.com/PHPOffice/PhpSpreadsheet/pull/4257)
|
||||
@@ -342,18 +522,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
## 2024-11-22 - 3.5.0
|
||||
|
||||
### Added
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Changed
|
||||
|
||||
- Settings::libXmlLoaderOptions is ignored. [PR #4233](https://github.com/PHPOffice/PhpSpreadsheet/pull/4233)
|
||||
|
||||
### Moved
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Settings::setLibXmlLoaderOptions() and Settings::getLibXmlLoaderOptions() are no longer needed - no replacement.
|
||||
@@ -386,10 +558,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Worksheet::getProtectedCells was deprecated in release 2, but was not properly documented, and not removed in release 3. Use getProtectedCellRanges instead.
|
||||
- Writer/Html::isMpdf property was deprecated in release 2, but was not properly documented, and not removed in release 3. Use instanceof Mpdf instead.
|
||||
|
||||
### Moved
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Xls Writer Condtional Rules Applied to Whole Rows or Columns. [Issue #3185](https://github.com/PHPOffice/PhpSpreadsheet/issues/3185) [PR #4152](https://github.com/PHPOffice/PhpSpreadsheet/pull/4152)
|
||||
@@ -421,10 +589,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Currency and Accounting Wizards are changed to act like Excel, and a new CurrencyBase Wizard is added for for non-Excel formats. [Issue #4125](https://github.com/PHPOffice/PhpSpreadsheet/issues/4125) [Issue #4124](https://github.com/PHPOffice/PhpSpreadsheet/issues/4124) [PR #4127](https://github.com/PHPOffice/PhpSpreadsheet/pull/4127)
|
||||
- Images will not be added to spreadsheet if they cannot be validated as images.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Removed
|
||||
|
||||
- The following items were deprecated in release 2 and are now removed.
|
||||
@@ -433,10 +597,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Calculation\Calculation::_translateFormulaToLocale (use method name translateFormulaToLocale without leading underscore).
|
||||
- Calculation\Calculation::_translateFormulaToEnglish (use method name translateFormulaToEnglish without leading underscore).
|
||||
|
||||
### Moved
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Several security patches.
|
||||
@@ -446,22 +606,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
## 2024-08-07 - 2.2.2
|
||||
|
||||
### Added
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Moved
|
||||
|
||||
- Nothing yet.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Html Reader Preserve Unicode Whitespace. [Issue #1284](https://github.com/PHPOffice/PhpSpreadsheet/issues/1284) [PR #4106](https://github.com/PHPOffice/PhpSpreadsheet/pull/4106)
|
||||
@@ -500,10 +644,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
- Writer\Xls\Style\ColorMap is no longer needed.
|
||||
|
||||
### Moved
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Incorrect Reader CSV with BOM. [Issue #4028](https://github.com/PHPOffice/PhpSpreadsheet/issues/4028) [PR #4029](https://github.com/PHPOffice/PhpSpreadsheet/pull/4029)
|
||||
@@ -538,18 +678,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Default Style Alignment Property (workaround for bug in non-Excel spreadsheet apps) [Issue #3918](https://github.com/PHPOffice/PhpSpreadsheet/issues/3918) [PR #3924](https://github.com/PHPOffice/PhpSpreadsheet/pull/3924)
|
||||
- Additional Support for Date/Time Styles [PR #3939](https://github.com/PHPOffice/PhpSpreadsheet/pull/3939)
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Reader/Xml trySimpleXMLLoadString should not have had public visibility, and will be removed.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- IF Empty Arguments. [Issue #3875](https://github.com/PHPOffice/PhpSpreadsheet/issues/3875) [Issue #2146](https://github.com/PHPOffice/PhpSpreadsheet/issues/2146) [PR #3879](https://github.com/PHPOffice/PhpSpreadsheet/pull/3879)
|
||||
@@ -611,10 +743,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
- Functions `_translateFormulaToLocale` and `_translateFormulaEnglish` are replaced by versions without leading underscore. [PR #3828](https://github.com/PHPOffice/PhpSpreadsheet/pull/3828)
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Take advantage of mitoteam/jpgraph Extended mode to enable rendering of more graphs. [PR #3603](https://github.com/PHPOffice/PhpSpreadsheet/pull/3603)
|
||||
@@ -682,14 +810,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Allow `CellRange` and `CellAddress` objects for the `range` argument in the `rangeToArray()` method. [PR #3494](https://github.com/PHPOffice/PhpSpreadsheet/pull/3494)
|
||||
- Stock charts will now read and reproduce `upDownBars` and subsidiary tags; these were previously ignored on read and hard-coded on write. [PR #3515](https://github.com/PHPOffice/PhpSpreadsheet/pull/3515)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Updates Cell formula absolute ranges/references, and Defined Name absolute ranges/references when inserting/deleting rows/columns. [Issue #3368](https://github.com/PHPOffice/PhpSpreadsheet/issues/3368) [PR #3402](https://github.com/PHPOffice/PhpSpreadsheet/pull/3402)
|
||||
@@ -747,10 +867,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
- Rationalisation of Pre-defined Currency Format Masks [PR #3377](https://github.com/PHPOffice/PhpSpreadsheet/pull/3377)
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Calculation Engine doesn't evaluate Defined Name when default cell A1 is quote-prefixed [Issue #3335](https://github.com/PHPOffice/PhpSpreadsheet/issues/3335) [PR #3336](https://github.com/PHPOffice/PhpSpreadsheet/pull/3336)
|
||||
@@ -764,22 +880,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
## 1.27.1 - 2023-02-08
|
||||
|
||||
### Added
|
||||
|
||||
- Nothing
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Composer --dev dependency issue with dealerdirect/phpcodesniffer-composer-installer renaming their `master` branch to `main`
|
||||
@@ -794,14 +894,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Support for Structured References in the Calculation Engine [PR #3261](https://github.com/PHPOffice/PhpSpreadsheet/pull/3261)
|
||||
- Limited Support for Form Controls [PR #3130](https://github.com/PHPOffice/PhpSpreadsheet/pull/3130) [Issue #2396](https://github.com/PHPOffice/PhpSpreadsheet/issues/2396) [Issue #1770](https://github.com/PHPOffice/PhpSpreadsheet/issues/1770) [Issue #2388](https://github.com/PHPOffice/PhpSpreadsheet/issues/2388) [Issue #2904](https://github.com/PHPOffice/PhpSpreadsheet/issues/2904) [Issue #2661](https://github.com/PHPOffice/PhpSpreadsheet/issues/2661)
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Shared/JAMA is removed. [PR #3260](https://github.com/PHPOffice/PhpSpreadsheet/pull/3260)
|
||||
@@ -827,10 +919,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Xlsx Reader support for Pivot Tables [PR #2829](https://github.com/PHPOffice/PhpSpreadsheet/pull/2829)
|
||||
- Permit Date/Time Entered on Spreadsheet to be calculated as Float [Issue #1416](https://github.com/PHPOffice/PhpSpreadsheet/issues/1416) [PR #3121](https://github.com/PHPOffice/PhpSpreadsheet/pull/3121)
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Direct update of Calculation::suppressFormulaErrors is replaced with setter.
|
||||
@@ -838,10 +926,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- ExcelError public static variable errorCodes replaced with constant ERROR_CODES.
|
||||
- NumberFormat constant FORMAT_DATE_YYYYMMDD2 replaced with existing identical FORMAT_DATE_YYYYMMDD.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed handling for `_xlws` prefixed functions from Office365 [Issue #3245](https://github.com/PHPOffice/PhpSpreadsheet/issues/3245) [PR #3247](https://github.com/PHPOffice/PhpSpreadsheet/pull/3247)
|
||||
@@ -870,22 +954,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
|
||||
## 1.25.2 - 2022-09-25
|
||||
|
||||
### Added
|
||||
|
||||
- Nothing
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Composer dependency clash with ezyang/htmlpurifier
|
||||
@@ -918,10 +986,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT.
|
||||
- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077)
|
||||
@@ -950,18 +1014,6 @@ and this project adheres to [Semantic Versioning](https://semver.org). Thia is a
|
||||
- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0
|
||||
- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940)
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943)
|
||||
@@ -1002,14 +1054,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions
|
||||
- Improved performance for removing rows/columns from a worksheet
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827)
|
||||
@@ -1089,10 +1133,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method);
|
||||
and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencing them by passing the defined name of a named range as the argument.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Make allowance for the AutoFilter dropdown icon in the first row of an Autofilter range when using Autosize columns. [Issue #2413](https://github.com/PHPOffice/PhpSpreadsheet/issues/2413) [PR #2754](https://github.com/PHPOffice/PhpSpreadsheet/pull/2754)
|
||||
@@ -1160,14 +1200,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
- Extract some methods from the Calculation Engine into dedicated classes [#2537](https://github.com/PHPOffice/PhpSpreadsheet/issues/2537)
|
||||
- Eliminate calls to `flattenSingleValue()` that are no longer required when we're checking for array values as arguments [#2590](https://github.com/PHPOffice/PhpSpreadsheet/issues/2590)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed `ReferenceHelper@insertNewBefore` behavior when removing column before last column with null value [PR #2541](https://github.com/PHPOffice/PhpSpreadsheet/pull/2541)
|
||||
@@ -1202,14 +1234,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
|
||||
- Xlsx Writer will evaluate AutoFilter only if it is as yet unevaluated, or has changed since it was last evaluated [PR #2414](https://github.com/PHPOffice/PhpSpreadsheet/pull/2414)
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Rounding in `NumberFormatter` [Issue #2385](https://github.com/PHPOffice/PhpSpreadsheet/issues/2385) [PR #2399](https://github.com/PHPOffice/PhpSpreadsheet/pull/2399)
|
||||
@@ -1240,14 +1264,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
- Drop support for PHP 7.2, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support
|
||||
- Use native typing for objects that were already documented as such
|
||||
|
||||
### Deprecated
|
||||
|
||||
- Nothing
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed null conversation for strToUpper [#2292](https://github.com/PHPOffice/PhpSpreadsheet/issues/2292)
|
||||
@@ -1281,18 +1297,10 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
- Ability to stream to an Amazon S3 bucket [Issue #2249](https://github.com/PHPOffice/PhpSpreadsheet/issues/2249)
|
||||
- Provided a Size Helper class to validate size values (pt, px, em) [PR #1694](https://github.com/PHPOffice/PhpSpreadsheet/pull/1694)
|
||||
|
||||
### Changed
|
||||
|
||||
- Nothing.
|
||||
|
||||
### Deprecated
|
||||
|
||||
- PHP 8.1 will deprecate auto_detect_line_endings. As a result of this change, Csv Reader using some release after PHP8.1 will no longer be able to handle a Csv with Mac line endings.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Unexpected format in Xlsx Timestamp [Issue #2331](https://github.com/PHPOffice/PhpSpreadsheet/issues/2331) [PR #2332](https://github.com/PHPOffice/PhpSpreadsheet/pull/2332)
|
||||
@@ -1415,10 +1423,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
|
||||
This makes the logic in these classes easier to maintain; and will reduce the memory footprint required to execute formulae when calling these functions.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Avoid Duplicate Titles When Reading Multiple HTML Files.[Issue #1823](https://github.com/PHPOffice/PhpSpreadsheet/issues/1823) [PR #1829](https://github.com/PHPOffice/PhpSpreadsheet/pull/1829)
|
||||
@@ -1448,10 +1452,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
|
||||
- All Excel Function implementations in `Calculation\Database`, `Calculation\DateTime`, `Calculation\Engineering`, `Calculation\Financial`, `Calculation\Logical`, `Calculation\LookupRef`, `Calculation\MathTrig`, `Calculation\Statistical`, `Calculation\TextData` and `Calculation\Web` have been moved to dedicated classes for individual functions or groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed issue with absolute path in worksheets' Target [PR #1769](https://github.com/PHPOffice/PhpSpreadsheet/pull/1769)
|
||||
@@ -1511,10 +1511,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
- **IMPORTANT NOTE:** This Introduces a **BC break** in the handling of named ranges. Previously, a named range cell reference of `B2` would be treated identically to a named range cell reference of `$B2` or `B$2` or `$B$2` because the calculation engine treated then all as absolute references. These changes "fix" that, so the calculation engine now handles relative references in named ranges correctly.
|
||||
This change that resolves previously incorrect behaviour in the calculation may affect users who have dynamically defined named ranges using relative references when they should have used absolute references.
|
||||
|
||||
### Removed
|
||||
|
||||
- Nothing.
|
||||
|
||||
### Fixed
|
||||
|
||||
- PrintArea causes exception [#1544](https://github.com/phpoffice/phpspreadsheet/pull/1544)
|
||||
@@ -1525,10 +1521,6 @@ Note that this will be the last 1.x branch release before the 2.x release. We wi
|
||||
|
||||
## 1.14.1 - 2020-07-19
|
||||
|
||||
### Added
|
||||
|
||||
- nothing
|
||||
|
||||
### Fixed
|
||||
|
||||
- WEBSERVICE is HTTP client agnostic and must be configured via `Settings::setHttpClient()` [#1562](https://github.com/PHPOffice/PhpSpreadsheet/issues/1562)
|
||||
|
||||
+4
-4
@@ -43,7 +43,7 @@ This makes it easier to see exactly what is being tested when reviewing the PR.
|
||||
4. By default, Github removes markdown headings in the Release Notes. You can either edit to restore these, or, probably preferably, change the default comment character on your system - `git config core.commentChar ";"`.
|
||||
|
||||
> **Note:** Tagged releases are made from the `master` branch. Only in an emergency should a tagged release be made from the `release` branch. (i.e. cherry-picked hot-fixes.) However, there are 4 branches which have been updated to apply security patches, and those may be tagged if future security updates are needed.
|
||||
- release1291 (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
|
||||
- release210 (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
|
||||
- release222
|
||||
- release390
|
||||
- 1.30.x (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
|
||||
- 2.1.x (no further updates aside from security patches, including code changes needed for Php 8.5 compatibility)
|
||||
- 2.4.x (no further updates aside from security patches)
|
||||
- 3.10.x
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019-2025 PhpSpreadsheet Authors
|
||||
Copyright (c) 2019-2026 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
|
||||
|
||||
+3
-2
@@ -4,7 +4,7 @@
|
||||
[](https://coveralls.io/github/PHPOffice/PhpSpreadsheet?branch=master)
|
||||
[](https://packagist.org/packages/phpoffice/phpspreadsheet)
|
||||
[](https://packagist.org/packages/phpoffice/phpspreadsheet)
|
||||
[](https://packagist.org/packages/phpoffice/phpspreadsheet)
|
||||
[](https://packagist.org/packages/phpoffice/phpspreadsheet)
|
||||
[](https://gitter.im/PHPOffice/PhpSpreadsheet)
|
||||
|
||||
PhpSpreadsheet is a library written in pure PHP and offers a set of classes that
|
||||
@@ -16,8 +16,9 @@ This is the master branch, and is maintained for security and bug fixes.
|
||||
|
||||
LTS: For maintained branches, support for PHP versions will only be maintained for a period of six months beyond the
|
||||
[end of life](https://www.php.net/supported-versions) of that PHP version.
|
||||
Dropping support for versions after that date will not be considered a breaking change.
|
||||
|
||||
Currently the required PHP minimum version is PHP __8.1__, and we [will support that version](https://www.php.net/supported-versions.php) until 30th June 2026.
|
||||
Currently the required PHP minimum version is PHP __8.2__, and we [will support that version](https://www.php.net/supported-versions.php) until 30th June 2027.
|
||||
|
||||
See the `composer.json` for other requirements.
|
||||
|
||||
|
||||
+14
-8
@@ -13,7 +13,7 @@
|
||||
],
|
||||
"config": {
|
||||
"platform": {
|
||||
"php" : "8.1.99"
|
||||
"php": "8.2.99"
|
||||
},
|
||||
"process-timeout": 600,
|
||||
"sort-packages": true,
|
||||
@@ -42,6 +42,9 @@
|
||||
},
|
||||
{
|
||||
"name": "Adrien Crivelli"
|
||||
},
|
||||
{
|
||||
"name": "Owen Leibman"
|
||||
}
|
||||
],
|
||||
"scripts": {
|
||||
@@ -66,10 +69,11 @@
|
||||
]
|
||||
},
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.2",
|
||||
"ext-ctype": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-fileinfo": "*",
|
||||
"ext-filter": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-iconv": "*",
|
||||
"ext-libxml": "*",
|
||||
@@ -84,11 +88,11 @@
|
||||
"maennchen/zipstream-php": "^2.1 || ^3.0",
|
||||
"markbaker/complex": "^3.0",
|
||||
"markbaker/matrix": "^3.0",
|
||||
"psr/http-client": "^1.0",
|
||||
"psr/http-factory": "^1.0",
|
||||
"psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-intl": "*",
|
||||
"ext-openssl": "*",
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "dev-main",
|
||||
"dompdf/dompdf": "^2.0 || ^3.0",
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
@@ -98,12 +102,13 @@
|
||||
"phpstan/phpstan": "^1.1 || ^2.0",
|
||||
"phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
|
||||
"phpstan/phpstan-phpunit": "^1.0 || ^2.0",
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"phpunit/phpunit": "^10.5 || ^11.0",
|
||||
"squizlabs/php_codesniffer": "^3.7",
|
||||
"tecnickcom/tcpdf": "^6.5"
|
||||
"tecnickcom/tcpdf": ">=6.8.0 <7.0.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-intl": "PHP Internationalization Functions, regquired for NumberFormat Wizard",
|
||||
"ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
|
||||
"ext-openssl": "Handline Agile-encrypted Xlsx spreadsheets",
|
||||
"mpdf/mpdf": "Option for rendering PDF with PDF Writer",
|
||||
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||
"tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer",
|
||||
@@ -117,7 +122,8 @@
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"PhpOffice\\PhpSpreadsheetTests\\": "tests/PhpSpreadsheetTests",
|
||||
"PhpOffice\\PhpSpreadsheetInfra\\": "infra"
|
||||
"PhpOffice\\PhpSpreadsheetInfra\\": "infra",
|
||||
"PhpOffice\\PhpSpreadsheetBenchmarks\\": "tests/Benchmark"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class BinaryComparison
|
||||
@@ -41,17 +42,25 @@ class BinaryComparison
|
||||
return strcmp($str1, $str2);
|
||||
}
|
||||
|
||||
public static function compare(mixed $operand1, mixed $operand2, string $operator): bool
|
||||
public static function compare(mixed $operand1, mixed $operand2, string $operator): bool|string
|
||||
{
|
||||
// Simple validate the two operands if they are string values
|
||||
if (is_string($operand1) && $operand1 > '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) {
|
||||
$operand1 = Calculation::unwrapResult($operand1);
|
||||
}
|
||||
if (ErrorValue::isError($operand1, true)) {
|
||||
/** @var string $operand1 */
|
||||
return $operand1;
|
||||
}
|
||||
if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) {
|
||||
$operand2 = Calculation::unwrapResult($operand2);
|
||||
}
|
||||
if (ErrorValue::isError($operand2, true)) {
|
||||
/** @var string $operand2 */
|
||||
return $operand2;
|
||||
}
|
||||
|
||||
// Use case insensitive comparaison if not OpenOffice mode
|
||||
// Use case-insensitive comparison if not OpenOffice mode
|
||||
if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
if (is_string($operand1)) {
|
||||
$operand1 = StringHelper::strToUpper($operand1);
|
||||
|
||||
+193
-48
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use Composer\Pcre\Preg; // many pregs in this program use u modifier, which has side-effects which make it unsuitable for this
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger;
|
||||
@@ -13,6 +14,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\DefinedName;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\NamedRange;
|
||||
use PhpOffice\PhpSpreadsheet\ReferenceHelper;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
@@ -36,7 +38,7 @@ class Calculation extends CalculationLocale
|
||||
const CALCULATION_REGEXP_OPENBRACE = '\(';
|
||||
// Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it)
|
||||
const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?(?:_xlws\.)?((?:__xludf\.)?[\p{L}][\p{L}\p{N}\.]*)[\s]*\(';
|
||||
// Cell reference (cell or range of cells, with or without a sheet reference)
|
||||
// Cell reference, with or without a sheet reference)
|
||||
const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])';
|
||||
// Used only to detect spill operator #
|
||||
const CALCULATION_REGEXP_CELLREF_SPILL = '/' . self::CALCULATION_REGEXP_CELLREF . '#/i';
|
||||
@@ -88,9 +90,22 @@ class Calculation extends CalculationLocale
|
||||
*/
|
||||
private bool $calculationCacheEnabled = true;
|
||||
|
||||
/**
|
||||
* Maximum number of entries in the formula token cache.
|
||||
* Default 0 (disabled). Set via setFormulaTokenCacheMaxSize() to enable.
|
||||
*/
|
||||
private int $formulaTokenCacheMaxSize = 0;
|
||||
|
||||
/**
|
||||
* Cache of parsed formula tokens, keyed by the raw formula string.
|
||||
*
|
||||
* @var array<string, array<mixed>|bool>
|
||||
*/
|
||||
private array $formulaTokenCache = [];
|
||||
|
||||
private BranchPruner $branchPruner;
|
||||
|
||||
private bool $branchPruningEnabled = true;
|
||||
protected bool $branchPruningEnabled = true;
|
||||
|
||||
/**
|
||||
* List of operators that can be used within formulae
|
||||
@@ -239,6 +254,7 @@ class Calculation extends CalculationLocale
|
||||
{
|
||||
$this->clearCalculationCache();
|
||||
$this->branchPruner->clearBranchStore();
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -364,6 +380,44 @@ class Calculation extends CalculationLocale
|
||||
$this->calculationCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the formula token cache.
|
||||
*/
|
||||
public function clearFormulaTokenCache(): void
|
||||
{
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current number of entries in the formula token cache.
|
||||
*/
|
||||
public function getFormulaTokenCacheSize(): int
|
||||
{
|
||||
return count($this->formulaTokenCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the maximum number of entries in the formula token cache.
|
||||
* Set to 0 to disable caching (default), or a positive integer to enable.
|
||||
*/
|
||||
public function setFormulaTokenCacheMaxSize(int $size): self
|
||||
{
|
||||
$this->formulaTokenCacheMaxSize = max(0, $size);
|
||||
if ($this->formulaTokenCacheMaxSize === 0) {
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the maximum number of entries allowed in the formula token cache.
|
||||
*/
|
||||
public function getFormulaTokenCacheMaxSize(): int
|
||||
{
|
||||
return $this->formulaTokenCacheMaxSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear calculation cache for a specified worksheet.
|
||||
*/
|
||||
@@ -415,7 +469,7 @@ class Calculation extends CalculationLocale
|
||||
{
|
||||
if (is_string($value)) {
|
||||
// Error values cannot be "wrapped"
|
||||
if (preg_match('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
|
||||
if (Preg::isMatch('/^' . self::CALCULATION_REGEXP_ERROR . '$/i', $value, $match)) {
|
||||
// Return Excel errors "as is"
|
||||
return $value;
|
||||
}
|
||||
@@ -494,13 +548,13 @@ class Calculation extends CalculationLocale
|
||||
try {
|
||||
$value = $cell->getValue();
|
||||
if (is_string($value) && $cell->getDataType() === DataType::TYPE_FORMULA) {
|
||||
$value = preg_replace_callback(
|
||||
$value = Preg::replaceCallback(
|
||||
self::CALCULATION_REGEXP_CELLREF_SPILL,
|
||||
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
|
||||
$value
|
||||
);
|
||||
}
|
||||
$result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore-line
|
||||
$result = self::unwrapResult($this->_calculateFormulaValue($value, $cell->getCoordinate(), $cell)); //* @phpstan-ignore argument.type ($value can be mixed not string)
|
||||
if ($this->spreadsheet === null) {
|
||||
throw new Exception('null spreadsheet in calculateCellValue');
|
||||
}
|
||||
@@ -520,7 +574,7 @@ class Calculation extends CalculationLocale
|
||||
$cellAddress = array_pop($this->cellStack);
|
||||
}
|
||||
if ($this->spreadsheet !== null && is_array($cellAddress) && array_key_exists('sheet', $cellAddress)) {
|
||||
$sheetName = $cellAddress['sheet'] ?? null;
|
||||
$sheetName = $cellAddress['sheet'];
|
||||
$testSheet = is_string($sheetName) ? $this->spreadsheet->getSheetByName($sheetName) : null;
|
||||
if ($testSheet !== null && array_key_exists('cell', $cellAddress)) {
|
||||
/** @var array{cell: string} $cellAddress */
|
||||
@@ -557,11 +611,17 @@ class Calculation extends CalculationLocale
|
||||
*/
|
||||
public function parseFormula(string $formula): array|bool
|
||||
{
|
||||
$formula = preg_replace_callback(
|
||||
// Check the formula token cache first (only when caching is enabled)
|
||||
if ($this->formulaTokenCacheMaxSize > 0 && isset($this->formulaTokenCache[$formula])) {
|
||||
return $this->formulaTokenCache[$formula];
|
||||
}
|
||||
|
||||
$originalFormula = $formula;
|
||||
$formula = Preg::replaceCallback(
|
||||
self::CALCULATION_REGEXP_CELLREF_SPILL,
|
||||
fn (array $matches) => 'ANCHORARRAY(' . substr($matches[0], 0, -1) . ')',
|
||||
$formula
|
||||
) ?? $formula;
|
||||
);
|
||||
// Basic validation that this is indeed a formula
|
||||
// We return an empty array if not
|
||||
$formula = trim($formula);
|
||||
@@ -574,7 +634,21 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
|
||||
// Parse the formula and return the token stack
|
||||
return $this->internalParseFormula($formula);
|
||||
$result = $this->internalParseFormula($formula);
|
||||
|
||||
// Cache the result when caching is enabled (clear cache if it exceeds the maximum size)
|
||||
if ($this->formulaTokenCacheMaxSize > 0) {
|
||||
// Phpstan says if condition is always false,
|
||||
// but coverage report says next statement is covered.
|
||||
if (count($this->formulaTokenCache) >= $this->formulaTokenCacheMaxSize) {
|
||||
$this->formulaTokenCache = [];
|
||||
}
|
||||
// Cache key is the original formula string (before ANCHORARRAY transformation)
|
||||
// to ensure consistent lookup regardless of internal transformations.
|
||||
$this->formulaTokenCache[$originalFormula] = $result;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -657,8 +731,9 @@ class Calculation extends CalculationLocale
|
||||
return self::wrapResult((string) $formula);
|
||||
}
|
||||
|
||||
// https://www.reddit.com/r/excel/comments/chr41y/cmd_formula_stopped_working_since_last_update/
|
||||
if (preg_match('/^=\s*cmd\s*\|/miu', $formula) !== 0) {
|
||||
return self::wrapResult($formula);
|
||||
return ExcelError::REF(); // returns #BLOCKED in newer versions
|
||||
}
|
||||
|
||||
// Basic validation that this is indeed a formula
|
||||
@@ -914,13 +989,14 @@ class Calculation extends CalculationLocale
|
||||
$pad = $rpad = ', ';
|
||||
foreach ($value as $row) {
|
||||
if (is_array($row)) {
|
||||
$returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row));
|
||||
$returnMatrix[] = implode($pad, array_map([$this, 'showValue'], $row)); // @phpstan-ignore argument.type (array_map can theoretically return array<mixed> not array<string>)
|
||||
$rpad = '; ';
|
||||
} else {
|
||||
$returnMatrix[] = $this->showValue($row);
|
||||
}
|
||||
}
|
||||
|
||||
/** @var string[] $returnMatrix */
|
||||
return '{ ' . implode($rpad, $returnMatrix) . ' }';
|
||||
} elseif (is_string($value) && (trim($value, self::FORMULA_STRING_QUOTE) == $value)) {
|
||||
return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE;
|
||||
@@ -1052,6 +1128,50 @@ class Calculation extends CalculationLocale
|
||||
'>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
|
||||
];
|
||||
|
||||
/** @param array<?string> $matches */
|
||||
private function unionForComma(array $matches): string
|
||||
{
|
||||
$matches5 = (string) $matches[5];
|
||||
// Weirdly, the regexp which get us here for issue 4832
|
||||
// only gets us here for Php8.4+. 8.4 introduced
|
||||
// major changes for PCRE, but I cannot identify
|
||||
// the exact change which caused this discrepancy.
|
||||
// I do plan to update coverage to 8.4 at some point,
|
||||
// and I can remove the coverage annotations after that.
|
||||
// @codeCoverageIgnoreStart
|
||||
if (str_contains($matches5, '(') && !str_contains($matches5, ')')) {
|
||||
if ($this->spreadsheet !== null) {
|
||||
if ($this->spreadsheet->getSheetByName($matches5) === null) {
|
||||
$matches0 = (string) $matches[0];
|
||||
$this->debugLog->writeDebugLog('Not Reformulating %s', $matches0);
|
||||
|
||||
return $matches0;
|
||||
}
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
$matches1 = (string) $matches[1];
|
||||
$matches2 = (string) $matches[2];
|
||||
|
||||
return $matches1 . str_replace(',', '∪', $matches2);
|
||||
}
|
||||
|
||||
private const CELL_OR_CELLRANGE_OR_DEFINED_NAME
|
||||
= '(?:'
|
||||
. self::CALCULATION_REGEXP_CELLREF // cell address
|
||||
. '(?::' . self::CALCULATION_REGEXP_CELLREF . ')?' // optional range address, non-capturing
|
||||
. '|' . self::CALCULATION_REGEXP_DEFINEDNAME
|
||||
. ')'
|
||||
;
|
||||
|
||||
public const UNIONABLE_COMMAS = '/((?:[,(]|^)\s*)' // comma or open paren or start of string, followed by optional whitespace
|
||||
. '([(]' // open paren
|
||||
. self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
|
||||
. '(?:\s*,\s*' // optioonal whitespace, comma, optional whitespace, non-capturing
|
||||
. self::CELL_OR_CELLRANGE_OR_DEFINED_NAME // cell address
|
||||
. ')+' // one or more occurrences
|
||||
. '\s*[)])/i'; // optional whitespace, end paren
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>|false
|
||||
*/
|
||||
@@ -1060,6 +1180,12 @@ class Calculation extends CalculationLocale
|
||||
if (($formula = $this->convertMatrixReferences(trim($formula))) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$oldFormula = $formula;
|
||||
$formula = Preg::replaceCallback(self::UNIONABLE_COMMAS, $this->unionForComma(...), $formula);
|
||||
if ($oldFormula !== $formula) {
|
||||
$this->debugLog->writeDebugLog('Reformulated as %s', $formula);
|
||||
}
|
||||
$phpSpreadsheetFunctions = &self::getFunctionsAddress();
|
||||
|
||||
// If we're using cell caching, then $pCell may well be flushed back to the cache (which detaches the parent worksheet),
|
||||
@@ -1179,7 +1305,7 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
}
|
||||
} elseif (is_string($expectedArgumentCount) && $expectedArgumentCount !== '*') {
|
||||
if (1 !== preg_match('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
|
||||
if (!Preg::isMatch('/(\d*)([-+,])(\d*)/', $expectedArgumentCount, $argMatch)) {
|
||||
$argMatch = ['', '', '', ''];
|
||||
}
|
||||
switch ($argMatch[2]) {
|
||||
@@ -1236,16 +1362,14 @@ class Calculation extends CalculationLocale
|
||||
// because at least the braces are paired up (at this stage in the formula)
|
||||
// MS Excel allows this if the content is cell references; but doesn't allow actual values,
|
||||
// but at this point, we can't differentiate (so allow both)
|
||||
return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
/* The following code may be a better choice, but, with
|
||||
the other changes for this PR, I can no longer come up
|
||||
with a test case that gets here
|
||||
//return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
|
||||
$stack->push('Binary Operator', '∪');
|
||||
|
||||
++$index;
|
||||
$expectingOperator = false;
|
||||
|
||||
continue;*/
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @var array<string, int> $d */
|
||||
@@ -1267,11 +1391,12 @@ class Calculation extends CalculationLocale
|
||||
// do we now have a function/variable/number?
|
||||
$expectingOperator = true;
|
||||
$expectingOperand = false;
|
||||
$val = $match[1] ?? ''; //* @phpstan-ignore-line
|
||||
$val = $match[1] ?? '';
|
||||
$length = strlen($val);
|
||||
|
||||
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) {
|
||||
$val = (string) preg_replace('/\s/u', '', $val);
|
||||
// $val is known to be valid unicode from statement above, so Preg::replace is okay even with u modifier
|
||||
$val = Preg::replace('/\s/u', '', $val);
|
||||
if (isset($phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
||||
$valToUpper = strtoupper($val);
|
||||
} else {
|
||||
@@ -1410,7 +1535,7 @@ class Calculation extends CalculationLocale
|
||||
$refSheet = $pCellParent->getParentOrThrow()->getSheetByName($rangeSheetRef);
|
||||
}
|
||||
|
||||
if (ctype_digit($val) && $val <= 1048576) {
|
||||
if (ctype_digit($val) && $val <= AddressRange::MAX_ROW) {
|
||||
// Row range
|
||||
$stackItemType = 'Row Reference';
|
||||
$valx = $val;
|
||||
@@ -1419,7 +1544,7 @@ class Calculation extends CalculationLocale
|
||||
} elseif (ctype_alpha($val) && strlen($val) <= 3) {
|
||||
// Column range
|
||||
$stackItemType = 'Column Reference';
|
||||
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
|
||||
$endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow() : AddressRange::MAX_ROW; // Max 1,048,576 rows for Excel2007
|
||||
$val = "{$rangeWS2}{$val}{$endRowColRef}";
|
||||
}
|
||||
$stackItemReference = $val;
|
||||
@@ -1445,7 +1570,7 @@ class Calculation extends CalculationLocale
|
||||
// unescape any apostrophes or double quotes in worksheet name
|
||||
$val = str_replace(["''", '""'], ["'", '"'], $val);
|
||||
$column = 'A';
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevop must be non-null), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
$column = $pCellParent->getHighestDataColumn($val);
|
||||
}
|
||||
$val = "{$rowRangeReference[2]}{$column}{$rowRangeReference[7]}";
|
||||
@@ -1459,7 +1584,7 @@ class Calculation extends CalculationLocale
|
||||
// unescape any apostrophes or double quotes in worksheet name
|
||||
$val = str_replace(["''", '""'], ["'", '"'], $val);
|
||||
$row = '1';
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) {
|
||||
if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { // @phpstan-ignore booleanAnd.alwaysFalse (testPrevOp must be non-null?), booleanAnd.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
$row = $pCellParent->getHighestDataRow($val);
|
||||
}
|
||||
$val = "{$val}{$row}";
|
||||
@@ -1610,7 +1735,7 @@ class Calculation extends CalculationLocale
|
||||
foreach ($tokens as $tokenIdx => $tokenData) {
|
||||
/** @var mixed[] $tokenData */
|
||||
$this->processingAnchorArray = false;
|
||||
if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore-line
|
||||
if ($tokenData['type'] === 'Cell Reference' && isset($tokens[$tokenIdx + 1]) && $tokens[$tokenIdx + 1]['type'] === 'Operand Count for Function ANCHORARRAY()') { //* @phpstan-ignore offsetAccess.nonOffsetAccessible ($tokens might be mixed not array)
|
||||
$this->processingAnchorArray = true;
|
||||
}
|
||||
$token = $tokenData['value'];
|
||||
@@ -1712,7 +1837,7 @@ class Calculation extends CalculationLocale
|
||||
return $this->raiseFormulaError($e->getMessage(), $e->getCode(), $e);
|
||||
}
|
||||
}
|
||||
} elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore-line
|
||||
} elseif (!is_numeric($token) && !is_object($token) && isset($token, self::BINARY_OPERATORS[$token])) { //* @phpstan-ignore offsetAccess.invalidOffset ($token is mixed)
|
||||
// if the token is a binary operator, pop the top two values off the stack, do the operation, and push the result back on the stack
|
||||
// We must have two operands, error if we don't
|
||||
$operand2Data = $stack->pop();
|
||||
@@ -1830,7 +1955,7 @@ class Calculation extends CalculationLocale
|
||||
if ($breakNeeded) {
|
||||
break;
|
||||
}
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow);
|
||||
if ($pCellParent !== null && $this->spreadsheet !== null) {
|
||||
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($sheet1), false);
|
||||
} else {
|
||||
@@ -1901,7 +2026,7 @@ class Calculation extends CalculationLocale
|
||||
} elseif (Information\ErrorValue::isError($operand2)) {
|
||||
$result = $operand2;
|
||||
} else {
|
||||
$result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore-line
|
||||
$result = str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)); //* @phpstan-ignore binaryOp.invalid (unwrapresult can return mixed rather than string)
|
||||
$result = StringHelper::substring(
|
||||
$result,
|
||||
0,
|
||||
@@ -1934,8 +2059,8 @@ class Calculation extends CalculationLocale
|
||||
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
|
||||
$stack->push('Error', ExcelError::null(), null);
|
||||
} else {
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore-line
|
||||
. Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore-line
|
||||
$cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
|
||||
. Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); // @phpstan-ignore argument.type ($oCol or $oRow might be empty), argument.type (ditto)
|
||||
$this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect));
|
||||
$stack->push('Value', $cellIntersect, $cellRef);
|
||||
}
|
||||
@@ -1950,13 +2075,13 @@ class Calculation extends CalculationLocale
|
||||
|
||||
break;
|
||||
}
|
||||
} elseif (($token === '~') || ($token === '%')) {
|
||||
} elseif (($token === '~') || ($token === '%')) { // @phpstan-ignore booleanOr.alwaysFalse (phpstan says token can't be plain string), identical.alwaysFalse (ditto), identical.alwaysFalse (ditto)
|
||||
// if the token is a unary operator, pop one value off the stack, do the operation, and push it back on
|
||||
if (($arg = $stack->pop()) === null) {
|
||||
return $this->raiseFormulaError('Internal error - Operand value missing from stack');
|
||||
}
|
||||
$arg = $arg['value'];
|
||||
if ($token === '~') {
|
||||
if ($token === '~') { // @phpstan-ignore identical.alwaysFalse (phpstan says token can't be plain string)
|
||||
$this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg));
|
||||
$multiplier = -1;
|
||||
} else {
|
||||
@@ -1988,20 +2113,27 @@ class Calculation extends CalculationLocale
|
||||
} else {
|
||||
$this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack);
|
||||
}
|
||||
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
|
||||
} elseif (Preg::isMatch('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', StringHelper::convertToString($token ?? ''), $matches)) {
|
||||
$cellRef = null;
|
||||
|
||||
/* Phpstan says matches[8/9/10] is never set,
|
||||
and code coverage report seems to confirm.
|
||||
Appease PhpStan for now;
|
||||
probably delete this block later.
|
||||
regex101.com confirms - only 7 capturing groups.
|
||||
My theory is that this code expected regexp to
|
||||
match cell *or* cellRange, but it does not
|
||||
match the latter. Retain the code for now in case
|
||||
we do want to add the range match later.
|
||||
Probably delete this block later.
|
||||
Until delete happens, turn code coverage off.
|
||||
*/
|
||||
if (isset($matches[self::$matchIndex8])) {
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($cell === null) {
|
||||
// We can't access the range, so return a REF error
|
||||
$cellValue = ExcelError::REF();
|
||||
} else {
|
||||
$cellRef = $matches[6] . $matches[7] . ':' . $matches[self::$matchIndex9] . $matches[self::$matchIndex10];
|
||||
$matches[2] = (string) $matches[2];
|
||||
if ($matches[2] > '') {
|
||||
$matches[2] = trim($matches[2], "\"'");
|
||||
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
|
||||
@@ -2026,12 +2158,14 @@ class Calculation extends CalculationLocale
|
||||
$this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue));
|
||||
}
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
} else {
|
||||
if ($cell === null) {
|
||||
// We can't access the cell, so return a REF error
|
||||
$cellValue = ExcelError::REF();
|
||||
} else {
|
||||
$cellRef = $matches[6] . $matches[7];
|
||||
$matches[2] = (string) $matches[2];
|
||||
if ($matches[2] > '') {
|
||||
$matches[2] = trim($matches[2], "\"'");
|
||||
if ((str_contains($matches[2], '[')) || (str_contains($matches[2], ']'))) {
|
||||
@@ -2042,7 +2176,11 @@ class Calculation extends CalculationLocale
|
||||
if ($pCellParent !== null && $this->spreadsheet !== null) {
|
||||
$cellSheet = $this->spreadsheet->getSheetByName($matches[2]);
|
||||
if ($cellSheet && !$cellSheet->cellExists($cellRef)) {
|
||||
$cellSheet->setCellValue($cellRef, null);
|
||||
try {
|
||||
$cellSheet->setCellValue($cellRef, null);
|
||||
} catch (SpreadsheetException) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
if ($cellSheet && $cellSheet->cellExists($cellRef)) {
|
||||
$cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false);
|
||||
@@ -2073,7 +2211,7 @@ class Calculation extends CalculationLocale
|
||||
$cellValue = array_shift($cellValue);
|
||||
}
|
||||
if (is_string($cellValue)) {
|
||||
$cellValue = preg_replace('/"/', '""', $cellValue);
|
||||
$cellValue = Preg::replace('/"/', '""', $cellValue);
|
||||
}
|
||||
$this->debugLog->writeDebugLog('Scalar Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue));
|
||||
}
|
||||
@@ -2117,7 +2255,7 @@ class Calculation extends CalculationLocale
|
||||
$a = $argCount - $i - 1;
|
||||
if (
|
||||
($passByReference)
|
||||
&& (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore-line
|
||||
&& (isset($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])) //* @phpstan-ignore offsetAccess.nonOffsetAccessible (possibly need to pass 2 arguments to isset)
|
||||
&& ($phpSpreadsheetFunctions[$functionName]['passByReference'][$a])
|
||||
) {
|
||||
/** @var mixed[] $arg */
|
||||
@@ -2184,7 +2322,7 @@ class Calculation extends CalculationLocale
|
||||
if ($functionName !== 'MKMATRIX') {
|
||||
if ($this->debugLog->getWriteDebugLog()) {
|
||||
krsort($argArrayVals);
|
||||
$this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)));
|
||||
$this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); // @phpstan-ignore argument.type (flattenArray returns array<mixed> rather than array<string>)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2221,7 +2359,7 @@ class Calculation extends CalculationLocale
|
||||
}
|
||||
} else {
|
||||
// if the token is a number, boolean, string or an Excel error, push it onto the stack
|
||||
/** @var ?string $token */
|
||||
/** @var null|numeric-string $token */
|
||||
if (isset(self::EXCEL_CONSTANTS[strtoupper($token ?? '')])) {
|
||||
$excelConstant = strtoupper("$token");
|
||||
$stack->push('Constant Value', self::EXCEL_CONSTANTS[$excelConstant]);
|
||||
@@ -2229,7 +2367,7 @@ class Calculation extends CalculationLocale
|
||||
$branchStore[$storeKey] = self::EXCEL_CONSTANTS[$excelConstant];
|
||||
}
|
||||
$this->debugLog->writeDebugLog('Evaluating Constant %s as %s', $excelConstant, $this->showTypeDetails(self::EXCEL_CONSTANTS[$excelConstant]));
|
||||
} elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore-line
|
||||
} elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { //* @phpstan-ignore function.alreadyNarrowedType (phpstan says is_bool has argument of *NEVER*?), equal.alwaysFalse (ditto)
|
||||
/** @var array{type: string, reference: ?string} $tokenData */
|
||||
$stack->push($tokenData['type'], $token, $tokenData['reference']);
|
||||
if (isset($storeKey)) {
|
||||
@@ -2355,7 +2493,7 @@ class Calculation extends CalculationLocale
|
||||
$r = $stack->pop();
|
||||
$result[$x] = $r['value'];
|
||||
}
|
||||
} elseif (is_array($operand2) && is_array($operand1)) {
|
||||
} elseif (is_array($operand2) /*&& is_array($operand1)*/) {
|
||||
// Operand 1 and Operand 2 are both arrays
|
||||
if (!$recursingArrays) {
|
||||
self::checkMatrixOperands($operand1, $operand2, 2);
|
||||
@@ -2368,7 +2506,7 @@ class Calculation extends CalculationLocale
|
||||
$result[$x] = $r['value'];
|
||||
}
|
||||
} else {
|
||||
throw new Exception('Neither operand is an arra');
|
||||
throw new Exception('Neither operand is an array');
|
||||
}
|
||||
// Log the result details
|
||||
$this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result));
|
||||
@@ -2378,8 +2516,8 @@ class Calculation extends CalculationLocale
|
||||
return $result;
|
||||
}
|
||||
|
||||
/** @return bool|mixed[] */
|
||||
private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool
|
||||
/** @return array<mixed>|bool|string */
|
||||
private function executeBinaryComparisonOperation(mixed $operand1, mixed $operand2, string $operation, Stack &$stack, bool $recursingArrays = false): array|bool|string
|
||||
{
|
||||
// If we're dealing with matrix operations, we want a matrix result
|
||||
if ((is_array($operand1)) || (is_array($operand2))) {
|
||||
@@ -2581,6 +2719,8 @@ class Calculation extends CalculationLocale
|
||||
if (!isset($aReferences[1])) {
|
||||
// Single cell in range
|
||||
sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow);
|
||||
/** @var string $currentCol */
|
||||
/** @var int $currentRow */
|
||||
if ($createCell && $worksheet !== null && !$worksheet->cellExists($aReferences[0])) {
|
||||
$worksheet->setCellValue($aReferences[0], null);
|
||||
}
|
||||
@@ -2600,6 +2740,8 @@ class Calculation extends CalculationLocale
|
||||
foreach ($aReferences as $reference) {
|
||||
// Extract range
|
||||
sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow);
|
||||
/** @var string $currentCol */
|
||||
/** @var int $currentRow */
|
||||
if ($createCell && $worksheet !== null && !$worksheet->cellExists($reference)) {
|
||||
$worksheet->setCellValue($reference, null);
|
||||
}
|
||||
@@ -2760,6 +2902,7 @@ class Calculation extends CalculationLocale
|
||||
// read constant value
|
||||
if (str_contains($constantName, '::')) {
|
||||
[$className, $constantName] = explode('::', $constantName);
|
||||
/** @var class-string $className */
|
||||
$constantReflector = new ReflectionClassConstant($className, $constantName);
|
||||
|
||||
return $constantReflector->getValue();
|
||||
@@ -2814,10 +2957,12 @@ class Calculation extends CalculationLocale
|
||||
$definedNameValue = $namedRange->getValue();
|
||||
$definedNameType = $namedRange->isFormula() ? 'Formula' : 'Range';
|
||||
if ($definedNameType === 'Range') {
|
||||
if (preg_match('/^(.*!)?(.*)$/', $definedNameValue, $matches) === 1) {
|
||||
$matches2 = trim($matches[2]);
|
||||
$matches2 = preg_replace('/ +/', ' ∩ ', $matches2) ?? $matches2;
|
||||
$matches2 = preg_replace('/,/', ' ∪ ', $matches2) ?? $matches2;
|
||||
if (Preg::isMatch('/^(.*!)?(.*)$/', $definedNameValue, $matches)) {
|
||||
$matches2 = Preg::replace(
|
||||
['/ +/', '/,/'],
|
||||
[' ∩ ', ' ∪ '],
|
||||
trim($matches[2])
|
||||
);
|
||||
$definedNameValue = $matches[1] . $matches2;
|
||||
}
|
||||
}
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
/**
|
||||
* A dedicated Calculation singleton for formula parsing only.
|
||||
*
|
||||
* This class provides an isolated instance specifically for parsing formulas
|
||||
* without branch pruning, avoiding state conflicts with the main Calculation
|
||||
* singleton used for cell value calculations.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class CalculationParserOnly extends Calculation
|
||||
{
|
||||
/**
|
||||
* Instance of this class.
|
||||
*/
|
||||
private static ?CalculationParserOnly $parserInstance = null;
|
||||
|
||||
/**
|
||||
* Branch pruning is disabled by default for parsing-only operations.
|
||||
*/
|
||||
protected bool $branchPruningEnabled = false;
|
||||
|
||||
/**
|
||||
* Get the singleton instance of this parser-only calculator.
|
||||
*/
|
||||
public static function getParserInstance(): self
|
||||
{
|
||||
if (!self::$parserInstance) {
|
||||
self::$parserInstance = new self();
|
||||
}
|
||||
|
||||
return self::$parserInstance;
|
||||
}
|
||||
|
||||
/** @param mixed $enabled Unused, property will always be false in this class */
|
||||
public function setBranchPruningEnabled(mixed $enabled): self
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
Vendored
+2
-2
@@ -53,7 +53,7 @@ abstract class DatabaseAbstract
|
||||
|
||||
/** @var callable */
|
||||
$callable = 'strtoupper';
|
||||
$fieldNames = array_map($callable, array_shift($database)); //* @phpstan-ignore-line
|
||||
$fieldNames = array_map($callable, array_shift($database)); //* @phpstan-ignore argument.type (array_shift can return mixed not array?)
|
||||
if (is_numeric($field)) {
|
||||
$field = (int) $field - 1;
|
||||
if ($field < 0 || $field >= count($fieldNames)) {
|
||||
@@ -143,7 +143,7 @@ abstract class DatabaseAbstract
|
||||
}
|
||||
|
||||
$rowQuery = array_map(
|
||||
fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore-line
|
||||
fn ($rowValue): string => (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : ($rowValue[0] ?? ''), // @phpstan-ignore nullCoalesce.offset ($rowValue[0] always exists?)
|
||||
$baseQuery
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ class Date
|
||||
* Excel Function:
|
||||
* DATE(year,month,day)
|
||||
*
|
||||
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non numeric values to this function.
|
||||
* PhpSpreadsheet is a lot more forgiving than MS Excel when passing non-numeric values to this function.
|
||||
* A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted,
|
||||
* as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language.
|
||||
*
|
||||
|
||||
+14
-5
@@ -46,13 +46,22 @@ class DateValue
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue);
|
||||
}
|
||||
|
||||
return self::fromString2($dateValue, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
|
||||
* depending on the value of the ReturnDateType flag
|
||||
*/
|
||||
public static function fromString2(null|string|int|bool|float $dateValue, ?int $calendar = null): array|string|float|int|DateTime
|
||||
{
|
||||
// try to parse as date iff there is at least one digit
|
||||
if (is_string($dateValue) && preg_match('/\d/', $dateValue) !== 1) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
$dti = new DateTimeImmutable();
|
||||
$baseYear = SharedDateHelper::getExcelCalendar();
|
||||
$baseYear = $calendar ?? SharedDateHelper::getExcelCalendar();
|
||||
$dateValue = trim((string) $dateValue, '"');
|
||||
// Strip any ordinals because they're allowed in Excel (English only)
|
||||
$dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue);
|
||||
@@ -86,7 +95,7 @@ class DateValue
|
||||
return self::finalResults($PHPDateArray, $dti, $baseYear);
|
||||
}
|
||||
|
||||
/** @param mixed[] $t1 */
|
||||
/** @param list<float|int|string> $t1 */
|
||||
private static function t1ToString(array $t1, DateTimeImmutable $dti, bool $yearFound): string
|
||||
{
|
||||
if (count($t1) == 2) {
|
||||
@@ -118,7 +127,7 @@ class DateValue
|
||||
if (!Helpers::dateParseSucceeded($PHPDateArray)) {
|
||||
// If original count was 1, we've already returned.
|
||||
// If it was 2, we added another.
|
||||
// Therefore, neither of the first 2 stroks below can fail.
|
||||
// Therefore, neither of the first 2 strtoks below can fail.
|
||||
$testVal1 = strtok($dateValue, '- ');
|
||||
$testVal2 = strtok('- ');
|
||||
$testVal3 = strtok('- ') ?: $dti->format('Y');
|
||||
@@ -160,9 +169,9 @@ class DateValue
|
||||
$day = self::getInt($PHPDateArray, 'day');
|
||||
$year = self::getInt($PHPDateArray, 'year');
|
||||
if (!checkdate($month, $day, $year)) {
|
||||
return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE();
|
||||
return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0, calendar: $baseYear) : ExcelError::VALUE();
|
||||
}
|
||||
$retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true);
|
||||
$retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true, calendar: $baseYear);
|
||||
}
|
||||
|
||||
return $retValue;
|
||||
|
||||
+3
-5
@@ -50,11 +50,9 @@ class Days
|
||||
|
||||
$days = ExcelError::VALUE();
|
||||
$diff = $PHPStartDateObject->diff($PHPEndDateObject);
|
||||
if (!is_bool($diff->days)) {
|
||||
$days = $diff->days;
|
||||
if ($diff->invert) {
|
||||
$days = -$days;
|
||||
}
|
||||
$days = $diff->days;
|
||||
if ($diff->invert) {
|
||||
$days = -$days;
|
||||
}
|
||||
|
||||
return $days;
|
||||
|
||||
+1
-1
@@ -76,7 +76,7 @@ class Days360
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of days between two dates based on a 360 day calendar.
|
||||
* Return the number of days between two dates based on a 360-day calendar.
|
||||
*/
|
||||
private static function dateDiff360(int $startDay, int $startMonth, int $startYear, int $endDay, int $endMonth, int $endYear, bool $methodUS): int
|
||||
{
|
||||
|
||||
+28
-19
@@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
|
||||
use Throwable;
|
||||
|
||||
class Helpers
|
||||
{
|
||||
@@ -29,10 +30,10 @@ class Helpers
|
||||
*
|
||||
* @return float Excel date/time serial value
|
||||
*/
|
||||
public static function getDateValue(mixed $dateValue, bool $allowBool = true): float
|
||||
public static function getDateValue(mixed $dateValue, bool $allowBool = true, ?int $calendar = null): float
|
||||
{
|
||||
if (is_object($dateValue)) {
|
||||
$retval = SharedDateHelper::PHPToExcel($dateValue);
|
||||
$retval = SharedDateHelper::PHPToExcel($dateValue, calendar: $calendar);
|
||||
if (is_bool($retval)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
@@ -40,7 +41,7 @@ class Helpers
|
||||
return $retval;
|
||||
}
|
||||
|
||||
self::nullFalseTrueToNumber($dateValue, $allowBool);
|
||||
self::nullFalseTrueToNumber($dateValue, $allowBool, $calendar);
|
||||
if (!is_numeric($dateValue)) {
|
||||
$saveReturnDateType = Functions::getReturnDateType();
|
||||
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
|
||||
@@ -56,6 +57,12 @@ class Helpers
|
||||
throw new Exception(ExcelError::NAN());
|
||||
}
|
||||
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject((float) $dateValue, calendar: $calendar);
|
||||
} catch (Throwable) {
|
||||
throw new Exception(ExcelError::NAN());
|
||||
}
|
||||
|
||||
return (float) $dateValue;
|
||||
}
|
||||
|
||||
@@ -80,10 +87,10 @@ class Helpers
|
||||
*
|
||||
* @param float|int $dateValue date to be adjusted
|
||||
*/
|
||||
public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0): DateTime
|
||||
public static function adjustDateByMonths($dateValue = 0, float $adjustmentMonths = 0, ?int $calendar = null): DateTime
|
||||
{
|
||||
// Execute function
|
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue);
|
||||
$PHPDateObject = SharedDateHelper::excelToDateTimeObject($dateValue, calendar: $calendar);
|
||||
$oMonth = (int) $PHPDateObject->format('m');
|
||||
$oYear = (int) $PHPDateObject->format('Y');
|
||||
|
||||
@@ -134,7 +141,7 @@ class Helpers
|
||||
*
|
||||
* @param array{year: int, month: int, day: int, hour: int, minute: int, second: int} $dateArray
|
||||
*/
|
||||
public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false): DateTime|float|int
|
||||
public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false, ?int $calendar = null): DateTime|float|int
|
||||
{
|
||||
$retType = Functions::getReturnDateType();
|
||||
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
|
||||
@@ -154,58 +161,60 @@ class Helpers
|
||||
$dateArray['day'],
|
||||
$dateArray['hour'],
|
||||
$dateArray['minute'],
|
||||
$dateArray['second']
|
||||
$dateArray['second'],
|
||||
calendar: $calendar
|
||||
);
|
||||
if ($retType === Functions::RETURNDATE_EXCEL) {
|
||||
return $noFrac ? floor($excelDateValue) : $excelDateValue;
|
||||
}
|
||||
// RETURNDATE_UNIX_TIMESTAMP)
|
||||
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue);
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return result in one of three formats.
|
||||
*/
|
||||
public static function returnIn3FormatsFloat(float $excelDateValue): float|int|DateTime
|
||||
public static function returnIn3FormatsFloat(float $excelDateValue, ?int $calendar = null): float|int|DateTime
|
||||
{
|
||||
$retType = Functions::getReturnDateType();
|
||||
if ($retType === Functions::RETURNDATE_EXCEL) {
|
||||
return $excelDateValue;
|
||||
}
|
||||
if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue);
|
||||
return SharedDateHelper::excelToTimestamp($excelDateValue, calendar: $calendar);
|
||||
}
|
||||
// RETURNDATE_PHP_DATETIME_OBJECT
|
||||
|
||||
return SharedDateHelper::excelToDateTimeObject($excelDateValue);
|
||||
return SharedDateHelper::excelToDateTimeObject($excelDateValue, calendar: $calendar);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return result in one of three formats.
|
||||
*/
|
||||
public static function returnIn3FormatsObject(DateTime $PHPDateObject): DateTime|float|int
|
||||
public static function returnIn3FormatsObject(DateTime $PHPDateObject, ?int $calendar = null): DateTime|float|int
|
||||
{
|
||||
$retType = Functions::getReturnDateType();
|
||||
if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) {
|
||||
return $PHPDateObject;
|
||||
}
|
||||
if ($retType === Functions::RETURNDATE_EXCEL) {
|
||||
return (float) SharedDateHelper::PHPToExcel($PHPDateObject);
|
||||
return (float) SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar);
|
||||
}
|
||||
// RETURNDATE_UNIX_TIMESTAMP
|
||||
$stamp = SharedDateHelper::PHPToExcel($PHPDateObject);
|
||||
$stamp = SharedDateHelper::PHPToExcel($PHPDateObject, calendar: $calendar);
|
||||
$stamp = is_bool($stamp) ? ((int) $stamp) : $stamp;
|
||||
|
||||
return SharedDateHelper::excelToTimestamp($stamp);
|
||||
return SharedDateHelper::excelToTimestamp($stamp, calendar: $calendar);
|
||||
}
|
||||
|
||||
private static function baseDate(): int
|
||||
private static function baseDate(?int $calendar): int
|
||||
{
|
||||
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
return 0;
|
||||
}
|
||||
if (SharedDateHelper::getExcelCalendar() === SharedDateHelper::CALENDAR_MAC_1904) {
|
||||
$calendar ??= SharedDateHelper::getExcelCalendar();
|
||||
if ($calendar === SharedDateHelper::CALENDAR_MAC_1904) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -215,10 +224,10 @@ class Helpers
|
||||
/**
|
||||
* Many functions accept null/false/true argument treated as 0/0/1.
|
||||
*/
|
||||
public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true): void
|
||||
public static function nullFalseTrueToNumber(mixed &$number, bool $allowBool = true, ?int $calendar = null): void
|
||||
{
|
||||
$number = Functions::flattenSingleValue($number);
|
||||
$nullVal = self::baseDate();
|
||||
$nullVal = self::baseDate($calendar);
|
||||
if ($number === null) {
|
||||
$number = $nullVal;
|
||||
} elseif ($allowBool && is_bool($number)) {
|
||||
|
||||
+17
@@ -4,7 +4,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper;
|
||||
use Throwable;
|
||||
|
||||
class TimeParts
|
||||
{
|
||||
@@ -44,6 +46,11 @@ class TimeParts
|
||||
}
|
||||
|
||||
// Execute function
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
} catch (Throwable) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
$timeValue = fmod($timeValue, 1);
|
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
SharedDateHelper::roundMicroseconds($timeValue);
|
||||
@@ -85,6 +92,11 @@ class TimeParts
|
||||
}
|
||||
|
||||
// Execute function
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
} catch (Throwable) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
$timeValue = fmod($timeValue, 1);
|
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
SharedDateHelper::roundMicroseconds($timeValue);
|
||||
@@ -126,6 +138,11 @@ class TimeParts
|
||||
}
|
||||
|
||||
// Execute function
|
||||
try {
|
||||
SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
} catch (Throwable) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
$timeValue = fmod($timeValue, 1);
|
||||
$timeValue = SharedDateHelper::excelToDateTimeObject($timeValue);
|
||||
SharedDateHelper::roundMicroseconds($timeValue);
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use Datetime;
|
||||
use DateTime;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
@@ -45,12 +45,12 @@ class TimeValue
|
||||
* Date information in time_text is ignored.
|
||||
* Or can be an array of date/time values
|
||||
*
|
||||
* @return array<mixed>|Datetime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
|
||||
* @return array<mixed>|DateTime|float|int|string Excel date/time serial value, PHP date/time serial value or PHP date/time object,
|
||||
* depending on the value of the ReturnDateType flag
|
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array
|
||||
* with the same dimensions
|
||||
*/
|
||||
public static function fromString(null|array|string|int|bool|float $timeValue): array|string|Datetime|int|float
|
||||
public static function fromString(null|array|string|int|bool|float $timeValue): array|string|DateTime|int|float
|
||||
{
|
||||
if (is_array($timeValue)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue);
|
||||
@@ -88,7 +88,7 @@ class TimeValue
|
||||
} elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) {
|
||||
$retValue = (int) SharedDateHelper::excelToTimestamp($excelDateValue + 25569) - 3600;
|
||||
} else {
|
||||
$retValue = new Datetime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
|
||||
$retValue = new DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -78,8 +78,8 @@ class Logger
|
||||
{
|
||||
// Only write the debug log if logging is enabled
|
||||
if ($this->writeDebugLog) {
|
||||
$message = sprintf($message, ...$args); //* @phpstan-ignore-line
|
||||
$cellReference = implode(' -> ', $this->cellStack->showStack());
|
||||
$message = sprintf($message, ...$args); //* @phpstan-ignore argument.type (...$args could contain items that sprintf can't handle)
|
||||
$cellReference = implode(' -> ', $this->cellStack->showStack()); //* @phpstan-ignore argument.type (showStack can return array<mixed> not array<string>)
|
||||
if ($this->echoDebugLog) {
|
||||
echo $cellReference,
|
||||
($this->cellStack->count() > 0 ? ' => ' : ''),
|
||||
|
||||
+6
-4
@@ -204,8 +204,9 @@ final class StructuredReference implements Operand, Stringable
|
||||
/** @var string $reference */
|
||||
$reference = str_replace('[' . self::ITEM_SPECIFIER_THIS_ROW . '],', '', $reference);
|
||||
|
||||
foreach ($this->columns as $columnId => $columnName) {
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnName); //* @phpstan-ignore-line
|
||||
foreach ($this->columns as $columnId => $columnNamex) {
|
||||
/** @var string $columnNamex */
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnNamex);
|
||||
$reference = $this->adjustRowReference($columnName, $reference, $cell, $columnId);
|
||||
}
|
||||
|
||||
@@ -338,8 +339,9 @@ final class StructuredReference implements Operand, Stringable
|
||||
private function getColumnsForColumnReference(string $reference, int $startRow, int $endRow): string
|
||||
{
|
||||
$columnsSelected = false;
|
||||
foreach ($this->columns as $columnId => $columnName) {
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnName ?? ''); //* @phpstan-ignore-line
|
||||
foreach ($this->columns as $columnId => $columnNamex) {
|
||||
/** @var ?string $columnNamex */
|
||||
$columnName = str_replace("\u{a0}", ' ', $columnNamex ?? '');
|
||||
$cellFrom = "{$columnId}{$startRow}";
|
||||
$cellTo = "{$columnId}{$endRow}";
|
||||
$cellReference = ($cellFrom === $cellTo) ? $cellFrom : "{$cellFrom}:{$cellTo}";
|
||||
|
||||
Vendored
+1
-1
@@ -150,7 +150,7 @@ class ConvertDecimal extends ConvertBase
|
||||
/**
|
||||
* toOctal.
|
||||
*
|
||||
* Return an decimal value as octal.
|
||||
* Return a decimal value as octal.
|
||||
*
|
||||
* Excel Function:
|
||||
* DEC2OCT(x[,places])
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class ErfC
|
||||
* Note: In Excel 2007 or earlier, if you input a negative value for the lower bound argument,
|
||||
* the function would return a #NUM! error. However, in Excel 2010, the function algorithm was
|
||||
* improved, so that it can now calculate the function for both positive and negative x values.
|
||||
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts nagative arguments.
|
||||
* PhpSpreadsheet follows Excel 2010 behaviour, and accepts negative arguments.
|
||||
*
|
||||
* Excel Function:
|
||||
* ERFC(x)
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ class Interest
|
||||
* =ISPMT(interest_rate, period, number_payments, pv)
|
||||
*
|
||||
* @param mixed $interestRate is the interest rate for the investment
|
||||
* @param mixed $period is the period to calculate the interest rate. It must be betweeen 1 and number_payments.
|
||||
* @param mixed $period is the period to calculate the interest rate. It must be between 1 and number_payments.
|
||||
* @param mixed $numberOfPeriods is the number of payments for the annuity
|
||||
* @param mixed $principleRemaining is the loan amount or present value of the payments
|
||||
*/
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Constant\Periodic;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants;
|
||||
|
||||
class InterestAndPrincipal
|
||||
{
|
||||
protected float $interest;
|
||||
|
||||
protected float $principal;
|
||||
|
||||
public function __construct(
|
||||
float $rate = 0.0,
|
||||
int $period = 0,
|
||||
int $numberOfPeriods = 0,
|
||||
float $presentValue = 0,
|
||||
float $futureValue = 0,
|
||||
int $type = FinancialConstants::PAYMENT_END_OF_PERIOD
|
||||
) {
|
||||
$payment = Payments::annuity($rate, $numberOfPeriods, $presentValue, $futureValue, $type);
|
||||
$capital = $presentValue;
|
||||
$interest = 0.0;
|
||||
$principal = 0.0;
|
||||
for ($i = 1; $i <= $period; ++$i) {
|
||||
$interest = ($type === FinancialConstants::PAYMENT_BEGINNING_OF_PERIOD && $i == 1) ? 0 : -$capital * $rate;
|
||||
$principal = (float) $payment - $interest;
|
||||
$capital += $principal;
|
||||
}
|
||||
|
||||
$this->interest = $interest;
|
||||
$this->principal = $principal;
|
||||
}
|
||||
|
||||
public function interest(): float
|
||||
{
|
||||
return $this->interest;
|
||||
}
|
||||
|
||||
public function principal(): float
|
||||
{
|
||||
return $this->principal;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -166,7 +166,7 @@ class NonPeriodic
|
||||
$foundneg = false;
|
||||
for ($i = 0; $i < $valCount; ++$i) {
|
||||
$fld = $values[$i];
|
||||
if (!is_numeric($fld)) { //* @phpstan-ignore-line
|
||||
if (!is_numeric($fld)) {
|
||||
return ExcelError::VALUE();
|
||||
} elseif ($fld > 0) {
|
||||
$foundpos = true;
|
||||
|
||||
+6
-15
@@ -21,8 +21,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
* whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
|
||||
* software or the use or other dealings in the software.
|
||||
*
|
||||
* The following links are no longer valid.
|
||||
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
|
||||
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
|
||||
*
|
||||
* @deprecated 5.5.0 No replacement.
|
||||
*/
|
||||
class FormulaParser
|
||||
{
|
||||
@@ -463,16 +466,8 @@ class FormulaParser
|
||||
$tokenCount = count($tokens1);
|
||||
for ($i = 0; $i < $tokenCount; ++$i) {
|
||||
$token = $tokens1[$i];
|
||||
if (isset($tokens1[$i - 1])) {
|
||||
$previousToken = $tokens1[$i - 1];
|
||||
} else {
|
||||
$previousToken = null;
|
||||
}
|
||||
if (isset($tokens1[$i + 1])) {
|
||||
$nextToken = $tokens1[$i + 1];
|
||||
} else {
|
||||
$nextToken = null;
|
||||
}
|
||||
$previousToken = $tokens1[$i - 1] ?? null;
|
||||
$nextToken = $tokens1[$i + 1] ?? null;
|
||||
|
||||
if ($token->getTokenType() != FormulaToken::TOKEN_TYPE_WHITESPACE) {
|
||||
$tokens2[] = $token;
|
||||
@@ -518,11 +513,7 @@ class FormulaParser
|
||||
$tokenCount = count($tokens2);
|
||||
for ($i = 0; $i < $tokenCount; ++$i) {
|
||||
$token = $tokens2[$i];
|
||||
if (isset($tokens2[$i - 1])) {
|
||||
$previousToken = $tokens2[$i - 1];
|
||||
} else {
|
||||
$previousToken = null;
|
||||
}
|
||||
$previousToken = $tokens2[$i - 1] ?? null;
|
||||
|
||||
if ($token->getTokenType() == FormulaToken::TOKEN_TYPE_OPERATORINFIX && $token->getValue() == '-') {
|
||||
if ($i == 0) {
|
||||
|
||||
+3
@@ -21,8 +21,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
* whether in an action of contract, tort or otherwise, arising from, out of or in connection with the
|
||||
* software or the use or other dealings in the software.
|
||||
*
|
||||
* The following links are no longer valid.
|
||||
* https://ewbi.blogs.com/develops/2007/03/excel_formula_p.html
|
||||
* https://ewbi.blogs.com/develops/2004/12/excel_formula_p.html
|
||||
*
|
||||
* @deprecated 5.5.0 No replacement.
|
||||
*/
|
||||
class FormulaToken
|
||||
{
|
||||
|
||||
+15
-12
@@ -9,7 +9,7 @@ class FunctionArray extends CalculationBase
|
||||
*
|
||||
* @var array<string, array{category: string, functionCall: string|string[], argumentCount: string, passCellReference?: bool, passByReference?: bool[], custom?: bool}>
|
||||
*/
|
||||
protected static array $phpSpreadsheetFunctions = [
|
||||
protected static array $phpSpreadsheetFunctions = [ // @phpstan-ignore property.defaultValue (Phpstan is wrong)
|
||||
'ABS' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
'functionCall' => [MathTrig\Absolute::class, 'evaluate'],
|
||||
@@ -149,7 +149,7 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'BAHTTEXT' => [
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [TextData\Thai::class, 'getBahtText'],
|
||||
'argumentCount' => '1',
|
||||
],
|
||||
'BASE' => [
|
||||
@@ -405,6 +405,7 @@ class FunctionArray extends CalculationBase
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [TextData\Concatenate::class, 'actualCONCATENATE'],
|
||||
'argumentCount' => '1+',
|
||||
'passCellReference' => true,
|
||||
],
|
||||
'CONFIDENCE' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
@@ -919,7 +920,7 @@ class FunctionArray extends CalculationBase
|
||||
'FLOOR' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
'functionCall' => [MathTrig\Floor::class, 'floor'],
|
||||
'argumentCount' => '1-2', // Excel requries 2, Ods/Gnumeric 1-2
|
||||
'argumentCount' => '1-2', // Excel requires 2, Ods/Gnumeric 1-2
|
||||
],
|
||||
'FLOOR.MATH' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
@@ -1284,8 +1285,9 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'INFO' => [
|
||||
'category' => Category::CATEGORY_INFORMATION,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Information\Info::class, 'getInfo'],
|
||||
'argumentCount' => '1',
|
||||
'passCellReference' => true,
|
||||
],
|
||||
'INT' => [
|
||||
'category' => Category::CATEGORY_MATH_AND_TRIG,
|
||||
@@ -2335,17 +2337,17 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'T.DIST' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDist'],
|
||||
'argumentCount' => '3',
|
||||
],
|
||||
'T.DIST.2T' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDistDot2T'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'T.DIST.RT' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotDistDotRT'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'TEXT' => [
|
||||
@@ -2425,12 +2427,12 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'T.INV' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'tDotInv'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'T.INV.2T' => [
|
||||
'category' => Category::CATEGORY_STATISTICAL,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'],
|
||||
'argumentCount' => '2',
|
||||
],
|
||||
'TODAY' => [
|
||||
@@ -2495,12 +2497,12 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'UNICHAR' => [
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'character'],
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'characterUnicode'],
|
||||
'argumentCount' => '1',
|
||||
],
|
||||
'UNICODE' => [
|
||||
'category' => Category::CATEGORY_TEXT_AND_DATA,
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'code'],
|
||||
'functionCall' => [TextData\CharacterConvert::class, 'codeUnicode'],
|
||||
'argumentCount' => '1',
|
||||
],
|
||||
'UNIQUE' => [
|
||||
@@ -2577,6 +2579,7 @@ class FunctionArray extends CalculationBase
|
||||
'category' => Category::CATEGORY_WEB,
|
||||
'functionCall' => [Web\Service::class, 'webService'],
|
||||
'argumentCount' => '1',
|
||||
'passCellReference' => true,
|
||||
],
|
||||
'WEEKDAY' => [
|
||||
'category' => Category::CATEGORY_DATE_AND_TIME,
|
||||
@@ -2625,7 +2628,7 @@ class FunctionArray extends CalculationBase
|
||||
],
|
||||
'XLOOKUP' => [
|
||||
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
|
||||
'functionCall' => [Functions::class, 'DUMMY'],
|
||||
'functionCall' => [LookupRef\XLookup::class, 'lookup'],
|
||||
'argumentCount' => '3-6',
|
||||
],
|
||||
'XNPV' => [
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\Date;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
@@ -360,4 +361,47 @@ class Functions
|
||||
|
||||
return $coordinate;
|
||||
}
|
||||
|
||||
/** @param mixed[] $array */
|
||||
public static function convertArrayToCellRange(array $array): string
|
||||
{
|
||||
$retVal = '';
|
||||
$lastRow = $lastColumn = $firstRow = $firstColumn = 0;
|
||||
foreach ($array as $rowkey => $row) {
|
||||
if (!is_array($row) || !is_int($rowkey) || $rowkey < 1) {
|
||||
$firstRow = 0;
|
||||
|
||||
break;
|
||||
}
|
||||
if ($firstRow > $rowkey || $firstRow === 0) {
|
||||
$firstRow = $rowkey;
|
||||
}
|
||||
if ($lastRow < $rowkey) {
|
||||
$lastRow = $rowkey;
|
||||
}
|
||||
foreach ($row as $colkey => $cellValue) {
|
||||
if (!preg_match('/^[A-Z]{1,3}$/', $colkey)) {
|
||||
$firstRow = 0;
|
||||
|
||||
break 2;
|
||||
}
|
||||
$column = Coordinate::columnIndexFromString($colkey);
|
||||
if ($firstColumn > $column || $firstColumn === 0) {
|
||||
$firstColumn = $column;
|
||||
}
|
||||
if ($lastColumn < $column) {
|
||||
$lastColumn = $column;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($firstRow > 0 && $firstColumn > 0 && ($firstRow !== $lastRow || $firstColumn !== $lastColumn)) {
|
||||
$retVal = Coordinate::stringFromColumnIndex($firstColumn)
|
||||
. $firstRow
|
||||
. ':'
|
||||
. Coordinate::stringFromColumnIndex($lastColumn)
|
||||
. $lastRow;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Information;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
|
||||
class Info
|
||||
{
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public static bool $infoSupported = true;
|
||||
|
||||
/**
|
||||
* INFO.
|
||||
*
|
||||
* Excel Function:
|
||||
* =INFO(type_text)
|
||||
*
|
||||
* @param mixed $typeText String specifying the type of information to be returned
|
||||
* @param ?Cell $cell Cell from which spreadsheet information is retrieved
|
||||
*
|
||||
* @return int|string The requested information about the current operating environment
|
||||
*/
|
||||
public static function getInfo(mixed $typeText = '', ?Cell $cell = null): int|string
|
||||
{
|
||||
if (!self::$infoSupported) {
|
||||
return Functions::DUMMY();
|
||||
}
|
||||
|
||||
return match (is_string($typeText) ? strtolower($typeText) : $typeText) {
|
||||
'directory' => '/',
|
||||
'numfile' => $cell?->getWorksheetOrNull()?->getParent()?->getSheetCount() ?? 1,
|
||||
'origin' => '$A:$A$1',
|
||||
'osversion' => 'PHP ' . PHP_VERSION,
|
||||
'recalc' => 'Automatic',
|
||||
'release' => PHP_VERSION,
|
||||
'system' => 'PHP',
|
||||
'memavail', 'memused', 'totmem' => ExcelError::NA(),
|
||||
default => ExcelError::VALUE(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+5
-3
@@ -52,8 +52,10 @@ class Value
|
||||
if (!empty($worksheet) && $cell->getWorksheet()->getParentOrThrow()->getSheetByName($worksheet) === null) {
|
||||
return false;
|
||||
}
|
||||
[$column, $row] = Coordinate::indexesFromString($cellValue ?? '');
|
||||
if ($column > 16384 || $row > 1048576) {
|
||||
|
||||
try {
|
||||
[$column, $row] = Coordinate::indexesFromString($cellValue ?? ''); //* @phpstan-ignore nullCoalesce.variable (Not sure whether Phpstan is correct)
|
||||
} catch (SpreadsheetException) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -265,7 +267,7 @@ class Value
|
||||
if (is_bool($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
if (is_string($value) && substr($value, 0, 1) === '#') {
|
||||
if (is_string($value) && str_starts_with($value, '#')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ class Conditional
|
||||
{
|
||||
$condition = ($condition === null) ? true : Functions::flattenSingleValue($condition);
|
||||
|
||||
if (ErrorValue::isError($condition)) {
|
||||
if (ErrorValue::isError($condition, true)) {
|
||||
return $condition;
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@ class ChooseRowsEtc
|
||||
*/
|
||||
public static function transpose(array $array): array
|
||||
{
|
||||
return empty($array) ? [] : (array_map((count($array) === 1) ? (fn ($x) => [$x]) : null, ...$array)); // @phpstan-ignore-line
|
||||
return empty($array) ? [] : (array_map((count($array) === 1) ? (fn ($x) => [$x]) : null, ...$array)); // @phpstan-ignore argument.type (phstan is wrong), argument.type (phpstan is wrong)
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
|
||||
+2
-2
@@ -58,7 +58,7 @@ class ExcelMatch
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
// MATCH() is not case sensitive, so we convert lookup value to be lower cased if it's a string type.
|
||||
// MATCH() is not case-sensitive, so we convert lookup value to be lower cased if it's a string type.
|
||||
if (is_string($lookupValue)) {
|
||||
$lookupValue = StringHelper::strToLower($lookupValue);
|
||||
}
|
||||
@@ -70,7 +70,7 @@ class ExcelMatch
|
||||
};
|
||||
|
||||
if ($valueKey !== null) {
|
||||
return ++$valueKey; //* @phpstan-ignore-line
|
||||
return ++$valueKey; //* @phpstan-ignore preInc.type ($valueKey can be mixed not numeric)
|
||||
}
|
||||
|
||||
// Unsuccessful in finding a match, return #N/A error value
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ class Filter
|
||||
*/
|
||||
private static function filterByRow(array $lookupArray, array $matchArray): array
|
||||
{
|
||||
$matchArray = array_values(array_column($matchArray, 0)); // @phpstan-ignore-line
|
||||
$matchArray = array_values(array_column($matchArray, 0));
|
||||
|
||||
return array_filter(
|
||||
array_values($lookupArray),
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class Hstack
|
||||
}
|
||||
}
|
||||
|
||||
$transpose = array_map(null, ...$inputData); //* @phpstan-ignore-line
|
||||
$transpose = array_map(null, ...$inputData); //* @phpstan-ignore argument.type (inputData is mixed not array)
|
||||
$returnMatrix = [];
|
||||
foreach ($transpose as $array) {
|
||||
$returnMatrix[] = Functions::flattenArray($array);
|
||||
|
||||
+13
-7
@@ -23,6 +23,13 @@ class Hyperlink
|
||||
*/
|
||||
public static function set(mixed $linkURL = '', mixed $displayName = null, ?Cell $cell = null): string
|
||||
{
|
||||
$worksheet = null;
|
||||
$coordinate = '';
|
||||
if ($cell !== null) {
|
||||
$coordinate = $cell->getCoordinate();
|
||||
$worksheet = $cell->getWorksheetOrNull();
|
||||
}
|
||||
|
||||
$linkURL = ($linkURL === null) ? '' : StringHelper::convertToString(Functions::flattenSingleValue($linkURL));
|
||||
$displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName);
|
||||
|
||||
@@ -30,17 +37,16 @@ class Hyperlink
|
||||
return ExcelError::REF();
|
||||
}
|
||||
|
||||
if (is_object($displayName)) {
|
||||
$displayName = $linkURL;
|
||||
}
|
||||
$displayName = StringHelper::convertToString($displayName);
|
||||
$displayName = StringHelper::convertToString($displayName, false);
|
||||
if (trim($displayName) === '') {
|
||||
$displayName = $linkURL;
|
||||
}
|
||||
|
||||
$cell->getHyperlink()
|
||||
->setUrl($linkURL);
|
||||
$cell->getHyperlink()->setTooltip($displayName);
|
||||
$worksheet?->getCell($coordinate)
|
||||
->getHyperlink()
|
||||
->setUrl($linkURL)
|
||||
->setTooltip($displayName)
|
||||
->setDisplay('');
|
||||
|
||||
return $displayName;
|
||||
}
|
||||
|
||||
+2
-2
@@ -106,14 +106,14 @@ class Indirect
|
||||
*/
|
||||
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
|
||||
{
|
||||
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
|
||||
return Calculation::getInstance($worksheet?->getParent())
|
||||
->extractCellRange($cellAddress, $worksheet, false, createCell: true);
|
||||
}
|
||||
|
||||
private static function handleRowColumnRanges(?Worksheet $worksheet, string $start, string $end): string
|
||||
{
|
||||
// Being lazy, we're only checking a single row/column to get the max
|
||||
if (ctype_digit($start) && $start <= 1048576) {
|
||||
if (ctype_digit($start) && $start <= AddressRange::MAX_ROW) {
|
||||
// Max 16,384 columns for Excel2007
|
||||
$endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : AddressRange::MAX_COLUMN;
|
||||
|
||||
|
||||
+6
-5
@@ -40,15 +40,15 @@ class Lookup
|
||||
$lookupColumns = self::columnCount($lookupVector);
|
||||
}
|
||||
|
||||
$resultVector = self::verifyResultVector($resultVector ?? $lookupVector); //* @phpstan-ignore-line
|
||||
$resultVector = self::verifyResultVector($resultVector ?? $lookupVector); //* @phpstan-ignore argument.type (resultVector can be mixed not array<array<mixed>>)
|
||||
|
||||
if ($lookupRows === 2 && !$hasResultVector) {
|
||||
$resultVector = array_pop($lookupVector);
|
||||
$lookupVector = array_shift($lookupVector);
|
||||
}
|
||||
|
||||
/** @var mixed[] $lookupVector */
|
||||
/** @var mixed[] $resultVector */
|
||||
/** @var array<int, mixed> $lookupVector */
|
||||
/** @var array<int, mixed> $resultVector */
|
||||
if ($lookupColumns !== 2) {
|
||||
$lookupVector = self::verifyLookupValues($lookupVector, $resultVector);
|
||||
}
|
||||
@@ -57,8 +57,8 @@ class Lookup
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $lookupVector
|
||||
* @param mixed[] $resultVector
|
||||
* @param array<int, mixed> $lookupVector
|
||||
* @param array<int, mixed> $resultVector
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
@@ -80,6 +80,7 @@ class Lookup
|
||||
if (is_array($dataValue2)) {
|
||||
$dataValue2 = array_shift($dataValue2);
|
||||
}
|
||||
/** @var int $key2 */
|
||||
$value = [$key1 => $dataValue1, $key2 => $dataValue2];
|
||||
}
|
||||
unset($value);
|
||||
|
||||
Vendored
+1
-1
@@ -11,7 +11,7 @@ class LookupRefValidations
|
||||
public static function validateInt(mixed $value): int
|
||||
{
|
||||
if (!is_numeric($value)) {
|
||||
if (is_string($value) && ErrorValue::isError($value)) {
|
||||
if (is_string($value) && ErrorValue::isError($value, true)) {
|
||||
throw new Exception($value);
|
||||
}
|
||||
|
||||
|
||||
+5
-5
@@ -125,7 +125,7 @@ class Matrix
|
||||
}
|
||||
|
||||
$rowKeys = array_keys($matrix);
|
||||
$columnKeys = @array_keys($matrix[$rowKeys[0]]); //* @phpstan-ignore-line
|
||||
$columnKeys = @array_keys($matrix[$rowKeys[0]]); //* @phpstan-ignore argument.type (argument 1 of array_keys may be mixed not array)
|
||||
|
||||
if ($columnNum > count($columnKeys)) {
|
||||
return ExcelError::REF();
|
||||
@@ -135,14 +135,14 @@ class Matrix
|
||||
return self::extractRowValue($matrix, $rowKeys, $rowNum);
|
||||
}
|
||||
|
||||
$columnNum = $columnKeys[--$columnNum]; //* @phpstan-ignore-line
|
||||
$columnNum = $columnKeys[--$columnNum]; //* @phpstan-ignore offsetAccess.notFound (perhaps index --$columnNum does not exist)
|
||||
if ($rowNum === 0) {
|
||||
return array_map(
|
||||
fn ($value): array => [$value],
|
||||
array_column($matrix, $columnNum)
|
||||
);
|
||||
}
|
||||
$rowNum = $rowKeys[--$rowNum]; //* @phpstan-ignore-line
|
||||
$rowNum = $rowKeys[--$rowNum]; //* @phpstan-ignore offsetAccess.notFound (perhaps index --$rowNum does not exist)
|
||||
/** @var mixed[][] $matrix */
|
||||
|
||||
return $matrix[$rowNum][$columnNum];
|
||||
@@ -150,7 +150,7 @@ class Matrix
|
||||
|
||||
/**
|
||||
* @param mixed[] $matrix
|
||||
* @param mixed[] $rowKeys
|
||||
* @param array<int, int> $rowKeys
|
||||
*/
|
||||
private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum): mixed
|
||||
{
|
||||
@@ -159,7 +159,7 @@ class Matrix
|
||||
}
|
||||
|
||||
$rowNum = $rowKeys[--$rowNum];
|
||||
$row = $matrix[$rowNum]; //* @phpstan-ignore-line
|
||||
$row = $matrix[$rowNum];
|
||||
if (is_array($row)) {
|
||||
return [$rowNum => $row];
|
||||
}
|
||||
|
||||
+3
-3
@@ -84,7 +84,7 @@ class Offset
|
||||
$endCellColumn = self::adjustEndCellColumnForWidth($endCellColumn, $width, $startCellColumn, $columns);
|
||||
$startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1);
|
||||
|
||||
$endCellRow = self::adustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow);
|
||||
$endCellRow = self::adjustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow);
|
||||
|
||||
if (($endCellRow <= 0) || ($endCellColumn < 0)) {
|
||||
return ExcelError::REF();
|
||||
@@ -102,7 +102,7 @@ class Offset
|
||||
/** @return mixed[] */
|
||||
private static function extractRequiredCells(?Worksheet $worksheet, string $cellAddress): array
|
||||
{
|
||||
return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null)
|
||||
return Calculation::getInstance($worksheet?->getParent())
|
||||
->extractCellRange($cellAddress, $worksheet, false);
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ class Offset
|
||||
* @param null|object|scalar $height
|
||||
* @param scalar $rows
|
||||
*/
|
||||
private static function adustEndCellRowForHeight($height, int $startCellRow, $rows, int $endCellRow): int
|
||||
private static function adjustEndCellRowForHeight($height, int $startCellRow, $rows, int $endCellRow): int
|
||||
{
|
||||
if (($height !== null) && (!is_object($height))) {
|
||||
$endCellRow = $startCellRow + (int) $height - 1;
|
||||
|
||||
Vendored
+4
-3
@@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
@@ -106,7 +107,7 @@ class RowColumnInformation
|
||||
if (self::cellAddressNullOrWhitespace($cellAddress)) {
|
||||
return 1;
|
||||
}
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress, true)) {
|
||||
return $cellAddress;
|
||||
}
|
||||
if (!is_array($cellAddress)) {
|
||||
@@ -131,7 +132,7 @@ class RowColumnInformation
|
||||
|
||||
private static function convert0ToName(int|string $result): int|string
|
||||
{
|
||||
if (is_int($result) && ($result <= 0 || $result > 1048576)) {
|
||||
if (is_int($result) && ($result <= 0 || $result > AddressRange::MAX_ROW)) {
|
||||
return ExcelError::NAME();
|
||||
}
|
||||
|
||||
@@ -211,7 +212,7 @@ class RowColumnInformation
|
||||
if (self::cellAddressNullOrWhitespace($cellAddress)) {
|
||||
return 1;
|
||||
}
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress)) {
|
||||
if (is_string($cellAddress) && ErrorValue::isError($cellAddress, true)) {
|
||||
return $cellAddress;
|
||||
}
|
||||
if (!is_array($cellAddress)) {
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ class Selection
|
||||
} else {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
$chosenEntry = floor($chosenEntry);
|
||||
$chosenEntry = (int) floor($chosenEntry);
|
||||
if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
+57
-21
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
@@ -20,6 +19,12 @@ class Sort extends LookupRefValidations
|
||||
* The returned array is the same shape as the provided array argument.
|
||||
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
|
||||
*
|
||||
* NOTE: If $sortArray contains a mixture of data types
|
||||
* (string/int/bool), the results may be unexpected.
|
||||
* This is also true if the array consists of string
|
||||
* representations of numbers, especially if there are
|
||||
* both positive and negative numbers in the mix.
|
||||
*
|
||||
* @param mixed $sortArray The range of cells being sorted
|
||||
* @param mixed $sortIndex The column or row number within the sortArray to sort on
|
||||
* @param mixed $sortOrder Flag indicating whether to sort ascending or descending
|
||||
@@ -32,8 +37,7 @@ class Sort extends LookupRefValidations
|
||||
public static function sort(mixed $sortArray, mixed $sortIndex = 1, mixed $sortOrder = self::ORDER_ASCENDING, mixed $byColumn = false): mixed
|
||||
{
|
||||
if (!is_array($sortArray)) {
|
||||
// Scalars are always returned "as is"
|
||||
return $sortArray;
|
||||
$sortArray = [[$sortArray]];
|
||||
}
|
||||
|
||||
/** @var mixed[][] */
|
||||
@@ -55,7 +59,7 @@ class Sort extends LookupRefValidations
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
// We want a simple, enumrated array of arrays where we can reference column by its index number.
|
||||
// We want a simple, enumerated array of arrays where we can reference column by its index number.
|
||||
/** @var callable(mixed): mixed */
|
||||
$temp = 'array_values';
|
||||
/** @var array<int> $sortOrder */
|
||||
@@ -72,6 +76,18 @@ class Sort extends LookupRefValidations
|
||||
* The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array.
|
||||
* The returned array is the same shape as the provided array argument.
|
||||
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
|
||||
* Microsoft doesn't even bother documenting that a column sort
|
||||
* is possible. However, it is. According to:
|
||||
* https://exceljet.net/functions/sortby-function
|
||||
* When by_array is a horizontal range, SORTBY sorts horizontally by columns.
|
||||
* My interpretation of this is that by_array must be an
|
||||
* array which contains exactly one row.
|
||||
*
|
||||
* NOTE: If the "byArray" contains a mixture of data types
|
||||
* (string/int/bool), the results may be unexpected.
|
||||
* This is also true if the array consists of string
|
||||
* representations of numbers, especially if there are
|
||||
* both positive and negative numbers in the mix.
|
||||
*
|
||||
* @param mixed $sortArray The range of cells being sorted
|
||||
* @param mixed $args
|
||||
@@ -87,8 +103,16 @@ class Sort extends LookupRefValidations
|
||||
public static function sortBy(mixed $sortArray, mixed ...$args): mixed
|
||||
{
|
||||
if (!is_array($sortArray)) {
|
||||
// Scalars are always returned "as is"
|
||||
return $sortArray;
|
||||
$sortArray = [[$sortArray]];
|
||||
}
|
||||
$transpose = false;
|
||||
$args0 = $args[0] ?? null;
|
||||
if (is_array($args0) && count($args0) === 1) {
|
||||
$args0 = reset($args0);
|
||||
if (is_array($args0) && count($args0) > 1) {
|
||||
$transpose = true;
|
||||
$sortArray = Matrix::transpose($sortArray);
|
||||
}
|
||||
}
|
||||
|
||||
$sortArray = self::enumerateArrayKeys($sortArray);
|
||||
@@ -99,14 +123,23 @@ class Sort extends LookupRefValidations
|
||||
try {
|
||||
$sortBy = $sortOrder = [];
|
||||
for ($i = 0; $i < $argumentCount; $i += 2) {
|
||||
$sortBy[] = self::validateSortVector($args[$i], $lookupArraySize);
|
||||
$argsI = $args[$i];
|
||||
if (!is_array($argsI)) {
|
||||
$argsI = [[$argsI]];
|
||||
}
|
||||
$sortBy[] = self::validateSortVector($argsI, $lookupArraySize);
|
||||
$sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
return self::processSortBy($sortArray, $sortBy, $sortOrder);
|
||||
$temp = self::processSortBy($sortArray, $sortBy, $sortOrder);
|
||||
if ($transpose) {
|
||||
$temp = Matrix::transpose($temp);
|
||||
}
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,10 +163,6 @@ class Sort extends LookupRefValidations
|
||||
|
||||
private static function validateScalarArgumentsForSort(mixed &$sortIndex, mixed &$sortOrder, int $sortArraySize): void
|
||||
{
|
||||
if (is_array($sortIndex) || is_array($sortOrder)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
$sortIndex = self::validatePositiveInt($sortIndex, false);
|
||||
|
||||
if ($sortIndex > $sortArraySize) {
|
||||
@@ -143,13 +172,13 @@ class Sort extends LookupRefValidations
|
||||
$sortOrder = self::validateSortOrder($sortOrder);
|
||||
}
|
||||
|
||||
/** @return mixed[] */
|
||||
private static function validateSortVector(mixed $sortVector, int $sortArraySize): array
|
||||
/**
|
||||
* @param mixed[] $sortVector
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
private static function validateSortVector(array $sortVector, int $sortArraySize): array
|
||||
{
|
||||
if (!is_array($sortVector)) {
|
||||
throw new Exception(ExcelError::VALUE());
|
||||
}
|
||||
|
||||
// It doesn't matter if it's a row or a column vectors, it works either way
|
||||
$sortVector = Functions::flattenArray($sortVector);
|
||||
if (count($sortVector) !== $sortArraySize) {
|
||||
@@ -203,12 +232,19 @@ class Sort extends LookupRefValidations
|
||||
*/
|
||||
private static function prepareSortVectorValues(array $sortVector): array
|
||||
{
|
||||
// Strings should be sorted case-insensitive; with booleans converted to locale-strings
|
||||
// Strings should be sorted case-insensitive.
|
||||
// Booleans are a complete mess. Excel always seems to sort
|
||||
// booleans in a mixed vector at either the top or the bottom,
|
||||
// so converting them to string or int doesn't really work.
|
||||
// Best advice is to use them in a boolean-only vector.
|
||||
// Code below chooses int conversion, which is sensible,
|
||||
// and, as a bonus, compatible with LibreOffice.
|
||||
return array_map(
|
||||
function ($value) {
|
||||
if (is_bool($value)) {
|
||||
return ($value) ? Calculation::getTRUE() : Calculation::getFALSE();
|
||||
} elseif (is_string($value)) {
|
||||
return (int) $value;
|
||||
}
|
||||
if (is_string($value)) {
|
||||
return StringHelper::strToLower($value);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -80,7 +80,7 @@ class TorowTocol
|
||||
continue;
|
||||
}
|
||||
$cell = 0;
|
||||
} elseif (ErrorValue::isError($cell)) {
|
||||
} elseif (ErrorValue::isError($cell, true)) {
|
||||
if ($ignore === 2 || $ignore === 3) {
|
||||
continue;
|
||||
}
|
||||
|
||||
+2
-2
@@ -40,7 +40,7 @@ class Unique
|
||||
// so implode each row into a single string value
|
||||
array_walk(
|
||||
$lookupVector,
|
||||
//* @phpstan-ignore-next-line
|
||||
// @phpstan-ignore argument.type (not sure what is needed)
|
||||
function (array &$value): void {
|
||||
$valuex = '';
|
||||
$separator = '';
|
||||
@@ -79,7 +79,7 @@ class Unique
|
||||
if (str_ends_with($stringValue, "\x01")) {
|
||||
// x01 should only end a string which is otherwise a float or int,
|
||||
// so phpstan is technically correct but what it fears should not happen.
|
||||
$stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore-line
|
||||
$stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore binaryOp.invalid (stringValue might not be numeric?)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ class Vstack
|
||||
if (!is_array($matrix)) {
|
||||
$count = 1;
|
||||
} else {
|
||||
$count = count(reset($matrix)); //* @phpstan-ignore-line
|
||||
$count = count(reset($matrix)); //* @phpstan-ignore argument.type (reset can theoretically return mixed not array)
|
||||
}
|
||||
$columns = max($columns, $count);
|
||||
}
|
||||
|
||||
+275
@@ -0,0 +1,275 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use UnhandledMatchError;
|
||||
|
||||
class XLookup extends LookupBase
|
||||
{
|
||||
//use ArrayEnabled; // not yet supported
|
||||
|
||||
/**
|
||||
* XLOOKUP — PHP emulation of Excel's XLOOKUP function.
|
||||
*
|
||||
* @param mixed $lookupValue Value to search for
|
||||
* @param mixed $lookupArray Expect array, Array to search in
|
||||
* @param mixed $returnArray Expect array, Array to return from (must match lookupArray size)
|
||||
* @param mixed $ifNotFound Value to return if no match found (default: #N/A!)
|
||||
* @param mixed $matchMode expect int 0 = exact match (default)
|
||||
* -1 = exact or next smaller
|
||||
* 1 = exact or next larger
|
||||
* 2 = wildcard match (* ? ~)
|
||||
* @param mixed $searchMode expect int 1 = first to last (default)
|
||||
* -1 = last to first
|
||||
* 2 = binary search ascending
|
||||
* -2 = binary search descending
|
||||
*/
|
||||
public static function lookup(
|
||||
mixed $lookupValue,
|
||||
mixed $lookupArray,
|
||||
mixed $returnArray,
|
||||
mixed $ifNotFound = '#N/A!',
|
||||
mixed $matchMode = 0,
|
||||
mixed $searchMode = 1
|
||||
): mixed {
|
||||
if (is_array($lookupValue)) {
|
||||
$lookupValue = Functions::flattenArray($lookupValue);
|
||||
if (count($lookupValue) === 1) {
|
||||
$lookupValue = reset($lookupValue);
|
||||
}
|
||||
}
|
||||
if (is_array($lookupValue)) {
|
||||
$result = [];
|
||||
foreach ($lookupValue as $value) {
|
||||
$result[] = self::lookup($value, $lookupArray, $returnArray, $ifNotFound, $matchMode, $searchMode);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
if (!is_array($lookupArray)) {
|
||||
$lookupArray = [$lookupArray];
|
||||
}
|
||||
if (!is_array($returnArray)) {
|
||||
$returnArray = [$returnArray];
|
||||
}
|
||||
$lookupArray = Functions::flattenArray($lookupArray);
|
||||
if (count($returnArray) === 1) {
|
||||
$returnArray = Functions::flattenArray($returnArray);
|
||||
} else {
|
||||
$oldArray = $returnArray;
|
||||
$returnArray = [];
|
||||
foreach ($oldArray as $row) {
|
||||
$newRow = Functions::flattenArray($row);
|
||||
if (count($newRow) === 1) {
|
||||
$newRow = reset($newRow);
|
||||
}
|
||||
$returnArray[] = $newRow;
|
||||
}
|
||||
}
|
||||
/*if (is_array($lookupValue)) { // not yet supported by ArrayEnabled
|
||||
return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $returnArray, $ifNotFound, $matchMode, $searchMode);
|
||||
}*/
|
||||
|
||||
try {
|
||||
self::validateLookupArray($lookupArray);
|
||||
self::validateLookupArray($returnArray);
|
||||
$matchMode = LookupRefValidations::validateInt($matchMode);
|
||||
$searchMode = LookupRefValidations::validateInt($searchMode);
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
if (!in_array($matchMode, [0, -1, 1, 2], true)) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
if (count($lookupArray) !== count($returnArray)) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
try {
|
||||
$index = match ($searchMode) {
|
||||
1 => self::searchLinear($lookupValue, $lookupArray, $matchMode, false),
|
||||
-1 => self::searchLinear($lookupValue, $lookupArray, $matchMode, true),
|
||||
2 => self::searchBinary($lookupValue, $lookupArray, $matchMode, true),
|
||||
-2 => self::searchBinary($lookupValue, $lookupArray, $matchMode, false),
|
||||
};
|
||||
} catch (UnhandledMatchError) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
return ($index === null) ? $ifNotFound : $returnArray[$index];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Search strategies
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Linear search (searchMode 1 and -1).
|
||||
*
|
||||
* @param mixed[] $lookupArray
|
||||
*/
|
||||
private static function searchLinear(
|
||||
mixed $lookupValue,
|
||||
array $lookupArray,
|
||||
int $matchMode,
|
||||
bool $reverse
|
||||
): ?int {
|
||||
|
||||
$keys = array_keys($lookupArray);
|
||||
if ($reverse) {
|
||||
$keys = array_reverse($keys);
|
||||
}
|
||||
|
||||
$bestIdx = null;
|
||||
$bestVal = null;
|
||||
|
||||
foreach ($keys as $i) {
|
||||
/** @var scalar */
|
||||
$candidate = $lookupArray[$i];
|
||||
|
||||
if ($matchMode === 2) {
|
||||
// Wildcard: convert Excel wildcards to PHP regex
|
||||
/** @var scalar $lookupValue */
|
||||
if (self::wildcardMatch((string) $lookupValue, (string) $candidate)) {
|
||||
return $i;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$cmp = self::compareValues($candidate, $lookupValue);
|
||||
|
||||
if ($cmp === 0) {
|
||||
return $i; // Exact match — return immediately
|
||||
}
|
||||
|
||||
if ($matchMode === -1 && $cmp < 0) {
|
||||
// Next smaller: track largest value still below lookupValue
|
||||
if ($bestVal === null || self::compareValues($candidate, $bestVal) > 0) {
|
||||
$bestVal = $candidate;
|
||||
$bestIdx = $i;
|
||||
}
|
||||
}
|
||||
|
||||
if ($matchMode === 1 && $cmp > 0) {
|
||||
// Next larger: track smallest value still above lookupValue
|
||||
if ($bestVal === null || self::compareValues($candidate, $bestVal) < 0) {
|
||||
$bestVal = $candidate;
|
||||
$bestIdx = $i;
|
||||
}
|
||||
}
|
||||
}
|
||||
/** @var ?int $bestIdx */
|
||||
|
||||
return $bestIdx;
|
||||
}
|
||||
|
||||
/**
|
||||
* Binary search (searchMode 2 and -2)
|
||||
* Assumes array is sorted ascending (searchMode 2) or descending (searchMode -2).
|
||||
*
|
||||
* @param mixed[] $lookupArray
|
||||
*/
|
||||
private static function searchBinary(
|
||||
mixed $lookupValue,
|
||||
array $lookupArray,
|
||||
int $matchMode,
|
||||
bool $ascending
|
||||
): ?int {
|
||||
|
||||
$values = array_values($lookupArray);
|
||||
$keys = array_keys($lookupArray);
|
||||
$lo = 0;
|
||||
$hi = count($values) - 1;
|
||||
$bestIdx = null;
|
||||
|
||||
while ($lo <= $hi) {
|
||||
$mid = intdiv($lo + $hi, 2);
|
||||
$cmp = self::compareValues($values[$mid], $lookupValue);
|
||||
|
||||
if (!$ascending) {
|
||||
$cmp = -$cmp; // Flip for descending
|
||||
}
|
||||
|
||||
if ($cmp === 0) {
|
||||
return $keys[$mid]; // Exact match
|
||||
}
|
||||
|
||||
if ($cmp < 0) {
|
||||
if ($matchMode === -1) {
|
||||
$bestIdx = $keys[$mid]; // Candidate for next smaller
|
||||
}
|
||||
$lo = $mid + 1;
|
||||
} else {
|
||||
if ($matchMode === 1) {
|
||||
$bestIdx = $keys[$mid]; // Candidate for next larger
|
||||
}
|
||||
$hi = $mid - 1;
|
||||
}
|
||||
}
|
||||
/** @var int $bestIdx */
|
||||
|
||||
return ($matchMode !== 0) ? $bestIdx : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two values with type coercion matching Excel's behaviour:
|
||||
* numbers < strings < booleans
|
||||
*/
|
||||
private static function compareValues(mixed $a, mixed $b): int
|
||||
{
|
||||
// Numeric comparison
|
||||
if (is_numeric($a) && is_numeric($b)) {
|
||||
return $a <=> $b;
|
||||
}
|
||||
// String comparison (case-insensitive, like Excel)
|
||||
if (is_string($a) && is_string($b)) {
|
||||
return strcasecmp($a, $b);
|
||||
}
|
||||
// Bool comparison
|
||||
if (is_bool($a) && is_bool($b)) {
|
||||
return $a <=> $b;
|
||||
}
|
||||
// Cross-type: number < string < bool
|
||||
$typeOrder = fn ($v) => match (true) {
|
||||
is_numeric($v) => 0,
|
||||
is_string($v) => 1,
|
||||
is_bool($v) => 2,
|
||||
default => 3,
|
||||
};
|
||||
|
||||
return $typeOrder($a) <=> $typeOrder($b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wildcard match (matchMode 2)
|
||||
* Supports Excel wildcards: * (any sequence), ? (any single char), ~ (escape).
|
||||
*/
|
||||
private static function wildcardMatch(string $pattern, string $subject): bool
|
||||
{
|
||||
// Handle ~* and ~? escapes first
|
||||
$regex = '';
|
||||
$len = strlen($pattern);
|
||||
for ($i = 0; $i < $len; ++$i) {
|
||||
$ch = $pattern[$i];
|
||||
if ($ch === '~' && $i + 1 < $len) {
|
||||
$next = $pattern[++$i];
|
||||
$regex .= preg_quote($next, '/');
|
||||
} elseif ($ch === '*') {
|
||||
$regex .= '.*';
|
||||
} elseif ($ch === '?') {
|
||||
$regex .= '.';
|
||||
} else {
|
||||
$regex .= preg_quote($ch, '/');
|
||||
}
|
||||
}
|
||||
|
||||
return (bool) preg_match('/^' . $regex . '$/i', $subject);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -57,7 +57,7 @@ class Arabic
|
||||
*
|
||||
* @param string|string[] $roman Should be a string, or can be an array of strings
|
||||
*
|
||||
* @return array<mixed>|int|string the arabic numberal contrived from the roman numeral
|
||||
* @return array<mixed>|int|string the arabic numeral contrived from the roman numeral
|
||||
* If an array of numbers is passed as the argument, then the returned result will also be an array
|
||||
* with the same dimensions
|
||||
*/
|
||||
|
||||
+2
-2
@@ -73,7 +73,7 @@ class Round
|
||||
return round(
|
||||
(float) (string) $number,
|
||||
$digits,
|
||||
RoundingMode::AwayFromZero //* @phpstan-ignore-line
|
||||
RoundingMode::AwayFromZero //* @phpstan-ignore argument.type (phpstan error till we no longer support Php < 8.4)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ class Round
|
||||
return round(
|
||||
(float) (string) $number,
|
||||
$digits,
|
||||
RoundingMode::TowardsZero //* @phpstan-ignore-line
|
||||
RoundingMode::TowardsZero
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+25
-1
@@ -32,6 +32,25 @@ class Subtotal
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
protected static function filterFilteredArgs(Cell $cellReference, array $args): array
|
||||
{
|
||||
return array_filter(
|
||||
$args,
|
||||
function ($index) use ($cellReference) {
|
||||
$explodeArray = explode('.', $index);
|
||||
$row = $explodeArray[1] ?? '';
|
||||
|
||||
return is_numeric($row) ? ($cellReference->getWorksheet()->getRowDimension((int) $row)->getVisibleAfterFilter()) : true;
|
||||
},
|
||||
ARRAY_FILTER_USE_KEY
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $args
|
||||
*
|
||||
@@ -125,13 +144,18 @@ class Subtotal
|
||||
if ($subtotal > 100) {
|
||||
$aArgs = self::filterHiddenArgs($cellReference, $aArgs);
|
||||
$subtotal -= 100;
|
||||
} else {
|
||||
$aArgs = self::filterFilteredArgs($cellReference, $aArgs);
|
||||
}
|
||||
|
||||
$aArgs = self::filterFormulaArgs($cellReference, $aArgs);
|
||||
if (array_key_exists($subtotal, self::CALL_FUNCTIONS)) {
|
||||
$call = self::CALL_FUNCTIONS[$subtotal];
|
||||
|
||||
return call_user_func_array($call, $aArgs); //* @phpstan-ignore-line
|
||||
/** @var float|int|string */
|
||||
$temp = call_user_func_array($call, $aArgs);
|
||||
|
||||
return $temp;
|
||||
}
|
||||
|
||||
return ExcelError::VALUE();
|
||||
|
||||
+2
-1
@@ -59,7 +59,7 @@ class Sum
|
||||
$returnValue += $arg;
|
||||
} elseif (is_bool($arg)) {
|
||||
$returnValue += (int) $arg;
|
||||
} elseif (ErrorValue::isError($arg)) {
|
||||
} elseif (ErrorValue::isError($arg, true)) {
|
||||
/** @var string $arg */
|
||||
return $arg;
|
||||
} elseif ($arg !== null && !Functions::isCellValue($k)) {
|
||||
@@ -110,6 +110,7 @@ class Sum
|
||||
}
|
||||
}
|
||||
|
||||
/** @var array<float|int> $wrkArray */
|
||||
return array_sum($wrkArray);
|
||||
}
|
||||
}
|
||||
|
||||
+3
-2
@@ -163,7 +163,8 @@ class Averages extends AggregateBase
|
||||
sort($aArgs, SORT_NUMERIC);
|
||||
$valueCount = $valueCount / 2;
|
||||
if ($valueCount == floor($valueCount)) {
|
||||
$returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2; //* @phpstan-ignore-line
|
||||
$valueCount = (int) $valueCount;
|
||||
$returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2;
|
||||
} else {
|
||||
$valueCount = (int) floor($valueCount);
|
||||
$returnValue = $aArgs[$valueCount];
|
||||
@@ -243,7 +244,7 @@ class Averages extends AggregateBase
|
||||
$maxfreqkey = $key;
|
||||
$maxfreqdatum = $datum;
|
||||
} elseif ($freq == $maxfreq) {
|
||||
if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { //* @phpstan-ignore-line
|
||||
if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { //* @phpstan-ignore offsetAccess.notFound (not sure what phpstan wants)
|
||||
$maxfreqkey = $key;
|
||||
$maxfreqdatum = $datum;
|
||||
}
|
||||
|
||||
+7
-3
@@ -10,6 +10,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Database\DSum;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class Conditional
|
||||
{
|
||||
@@ -26,11 +27,14 @@ class Conditional
|
||||
* AVERAGEIF(range,condition[, average_range])
|
||||
*
|
||||
* @param mixed $range Data values, expect array
|
||||
* @param null|mixed[]|string $condition the criteria that defines which cells will be checked
|
||||
* @param mixed $condition the criteria that defines which cells will be checked, expect null|mixed[]|string
|
||||
* @param mixed $averageRange Data values
|
||||
*/
|
||||
public static function AVERAGEIF(mixed $range, null|array|string $condition, mixed $averageRange = []): null|int|float|string
|
||||
public static function AVERAGEIF(mixed $range, mixed $condition, mixed $averageRange = []): null|int|float|string
|
||||
{
|
||||
if ($condition !== null && !is_array($condition)) {
|
||||
$condition = StringHelper::convertToString($condition);
|
||||
}
|
||||
if (!is_array($range) || !is_array($averageRange) || array_key_exists(0, $range) || array_key_exists(0, $averageRange)) {
|
||||
$refError = ExcelError::REF();
|
||||
if (in_array($refError, [$range, $averageRange], true)) {
|
||||
@@ -62,7 +66,7 @@ class Conditional
|
||||
return 0.0;
|
||||
}
|
||||
if (count($args) === 3) {
|
||||
return self::AVERAGEIF($args[1], $args[2], $args[0]); //* @phpstan-ignore-line
|
||||
return self::AVERAGEIF($args[1], $args[2], $args[0]);
|
||||
}
|
||||
foreach ($args as $arg) {
|
||||
if (is_array($arg) && array_key_exists(0, $arg)) {
|
||||
|
||||
Vendored
+8
-3
@@ -28,9 +28,9 @@ class Beta
|
||||
* Or can be an array of values
|
||||
* @param mixed $beta Parameter to the distribution as a float
|
||||
* Or can be an array of values
|
||||
* @param mixed $rMin as an float
|
||||
* @param mixed $rMin as a float
|
||||
* Or can be an array of values
|
||||
* @param mixed $rMax as an float
|
||||
* @param mixed $rMax as a float
|
||||
* Or can be an array of values
|
||||
*
|
||||
* @return array<mixed>|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
|
||||
@@ -116,6 +116,11 @@ class Beta
|
||||
if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0.0)) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
if (($alpha + $beta) > self::LOG_GAMMA_X_MAX_VALUE) {
|
||||
// incompleteBeta declines to evaluate here and returns 0 for every x,
|
||||
// so there is no quantile to search for.
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return self::calculateInverse($probability, $alpha, $beta, $rMin, $rMax);
|
||||
}
|
||||
@@ -130,7 +135,7 @@ class Beta
|
||||
while ((($b - $a) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) {
|
||||
$guess = ($a + $b) / 2;
|
||||
$result = self::distribution($guess, $alpha, $beta);
|
||||
if (($result === $probability) || ($result === 0.0)) {
|
||||
if ($result === $probability) {
|
||||
$b = $a;
|
||||
} elseif ($result > $probability) {
|
||||
$b = $guess;
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ class Binomial
|
||||
/**
|
||||
* BINOM.DIST.RANGE.
|
||||
*
|
||||
* Returns returns the Binomial Distribution probability for the number of successes from a specified number
|
||||
* Returns the Binomial Distribution probability for the number of successes from a specified number
|
||||
* of trials falling into a specified range.
|
||||
*
|
||||
* @param mixed $trials Integer number of trials
|
||||
|
||||
+13
-75
@@ -50,7 +50,7 @@ class ChiSquared
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2));
|
||||
return Gamma::regularizedGammaQ($degrees / 2, $value / 2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,8 +99,16 @@ class ChiSquared
|
||||
return 1 - (is_numeric($temp) ? $temp : 0);
|
||||
}
|
||||
|
||||
return ($value ** (($degrees / 2) - 1) * exp(-$value / 2))
|
||||
/ ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2));
|
||||
if ($value == 0.0) {
|
||||
if ($degrees === 2) {
|
||||
return 0.5;
|
||||
}
|
||||
|
||||
return ($degrees === 1) ? INF : 0.0;
|
||||
}
|
||||
|
||||
// Log domain, so large degrees of freedom cannot overflow Gamma(d/2).
|
||||
return exp((($degrees / 2) - 1) * log($value) - $value / 2 - ($degrees / 2) * M_LN2 - Gamma::logGamma($degrees / 2));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -133,8 +141,7 @@ class ChiSquared
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
$callback = fn (float $value): float => 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2)
|
||||
/ Gamma::gammaValue($degrees / 2));
|
||||
$callback = fn (float $value): float => Gamma::regularizedGammaQ($degrees / 2, $value / 2);
|
||||
|
||||
$newtonRaphson = new NewtonRaphson($callback);
|
||||
|
||||
@@ -258,75 +265,6 @@ class ChiSquared
|
||||
|
||||
private static function pchisq(float $chi2, int $degrees): float
|
||||
{
|
||||
return self::gammp($degrees, 0.5 * $chi2);
|
||||
}
|
||||
|
||||
private static function gammp(int $n, float $x): float
|
||||
{
|
||||
if ($x < 0.5 * $n + 1) {
|
||||
return self::gser($n, $x);
|
||||
}
|
||||
|
||||
return 1 - self::gcf($n, $x);
|
||||
}
|
||||
|
||||
// Return the incomplete gamma function P(n/2,x) evaluated by
|
||||
// series representation. Algorithm from numerical recipe.
|
||||
// Assume that n is a positive integer and x>0, won't check arguments.
|
||||
// Relative error controlled by the eps parameter
|
||||
private static function gser(int $n, float $x): float
|
||||
{
|
||||
/** @var float $gln */
|
||||
$gln = Gamma::ln($n / 2);
|
||||
$a = 0.5 * $n;
|
||||
$ap = $a;
|
||||
$sum = 1.0 / $a;
|
||||
$del = $sum;
|
||||
for ($i = 1; $i < 101; ++$i) {
|
||||
++$ap;
|
||||
$del = $del * $x / $ap;
|
||||
$sum += $del;
|
||||
if ($del < $sum * self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $sum * exp(-$x + $a * log($x) - $gln);
|
||||
}
|
||||
|
||||
// Return the incomplete gamma function Q(n/2,x) evaluated by
|
||||
// its continued fraction representation. Algorithm from numerical recipe.
|
||||
// Assume that n is a postive integer and x>0, won't check arguments.
|
||||
// Relative error controlled by the eps parameter
|
||||
private static function gcf(int $n, float $x): float
|
||||
{
|
||||
/** @var float $gln */
|
||||
$gln = Gamma::ln($n / 2);
|
||||
$a = 0.5 * $n;
|
||||
$b = $x + 1 - $a;
|
||||
$fpmin = 1.e-300;
|
||||
$c = 1 / $fpmin;
|
||||
$d = 1 / $b;
|
||||
$h = $d;
|
||||
for ($i = 1; $i < 101; ++$i) {
|
||||
$an = -$i * ($i - $a);
|
||||
$b += 2;
|
||||
$d = $an * $d + $b;
|
||||
if (abs($d) < $fpmin) {
|
||||
$d = $fpmin;
|
||||
}
|
||||
$c = $b + $an / $c;
|
||||
if (abs($c) < $fpmin) {
|
||||
$c = $fpmin;
|
||||
}
|
||||
$d = 1 / $d;
|
||||
$del = $d * $c;
|
||||
$h = $h * $del;
|
||||
if (abs($del - 1) < self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $h * exp(-$x + $a * log($x) - $gln);
|
||||
return Gamma::regularizedGammaP($degrees / 2, 0.5 * $chi2);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+15
-4
@@ -55,9 +55,20 @@ class F
|
||||
return Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2);
|
||||
}
|
||||
|
||||
return (Gamma::gammaValue(($v + $u) / 2)
|
||||
/ (Gamma::gammaValue($u / 2) * Gamma::gammaValue($v / 2)))
|
||||
* (($u / $v) ** ($u / 2))
|
||||
* (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2)));
|
||||
if ($value == 0.0) {
|
||||
if ($u === 2) {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
return ($u === 1) ? INF : 0.0;
|
||||
}
|
||||
|
||||
// Log domain, so large degrees of freedom cannot overflow the Gamma ratio.
|
||||
return exp(
|
||||
Gamma::logGamma(($v + $u) / 2) - Gamma::logGamma($u / 2) - Gamma::logGamma($v / 2)
|
||||
+ ($u / 2) * log($u / $v)
|
||||
+ (($u - 2) / 2) * log($value)
|
||||
- (($u + $v) / 2) * log(1 + ($u / $v) * $value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+8
-1
@@ -85,6 +85,10 @@ class Gamma extends GammaBase
|
||||
*
|
||||
* Returns the inverse of the Gamma distribution.
|
||||
*
|
||||
* For a probability so far into the upper tail that the forward-CDF series
|
||||
* approximation plateaus below it, the root cannot be bracketed; the result
|
||||
* is capped at the alpha*beta*5 search ceiling rather than diverging.
|
||||
*
|
||||
* @param mixed $probability Float probability at which you want to evaluate the distribution
|
||||
* Or can be an array of values
|
||||
* @param mixed $alpha Parameter to the distribution as a float
|
||||
@@ -143,6 +147,9 @@ class Gamma extends GammaBase
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return log(self::gammaValue($value));
|
||||
$result = self::logGamma($value);
|
||||
|
||||
// logGamma returns its MAX_VALUE sentinel when the result would overflow.
|
||||
return ($result >= self::MAX_VALUE) ? ExcelError::NAN() : $result;
|
||||
}
|
||||
}
|
||||
|
||||
+120
-12
@@ -11,7 +11,7 @@ abstract class GammaBase
|
||||
|
||||
private const EPS = 2.22e-16;
|
||||
|
||||
private const MAX_VALUE = 1.2e308;
|
||||
protected const MAX_VALUE = 1.2e308;
|
||||
|
||||
private const SQRT2PI = 2.5066282746310005024157652848110452530069867406099;
|
||||
|
||||
@@ -20,10 +20,19 @@ abstract class GammaBase
|
||||
protected static function calculateDistribution(float $value, float $a, float $b, bool $cumulative): float
|
||||
{
|
||||
if ($cumulative) {
|
||||
return self::incompleteGamma($a, $value / $b) / self::gammaValue($a);
|
||||
return self::regularizedGammaP($a, $value / $b);
|
||||
}
|
||||
|
||||
return (1 / ($b ** $a * self::gammaValue($a))) * $value ** ($a - 1) * exp(0 - ($value / $b));
|
||||
if ($value == 0.0) {
|
||||
if ($a == 1.0) {
|
||||
return 1.0 / $b;
|
||||
}
|
||||
|
||||
return ($a < 1.0) ? INF : 0.0;
|
||||
}
|
||||
|
||||
// Log domain, so large shape parameters cannot overflow Gamma(a).
|
||||
return exp(($a - 1.0) * log($value) - $value / $b - $a * log($b) - self::logGamma($a));
|
||||
}
|
||||
|
||||
/** @return float|string */
|
||||
@@ -32,6 +41,24 @@ abstract class GammaBase
|
||||
$xLo = 0;
|
||||
$xHi = $alpha * $beta * 5;
|
||||
|
||||
// Extend the upper bound while it does not yet bracket the root, so a
|
||||
// tail quantile beyond alpha*beta*5 is no longer clamped to it. Stop if
|
||||
// the CDF stops increasing (series approximation past its usable range)
|
||||
// and keep the original bound rather than expanding into that region.
|
||||
$xHiBase = $xHi;
|
||||
$cdfHi = self::calculateDistribution($xHi, $alpha, $beta, true);
|
||||
while ($cdfHi < $probability) {
|
||||
$xHiNext = $xHi * 2;
|
||||
$cdfNext = self::calculateDistribution($xHiNext, $alpha, $beta, true);
|
||||
if ($cdfNext <= $cdfHi) {
|
||||
$xHi = $xHiBase;
|
||||
|
||||
break;
|
||||
}
|
||||
$xHi = $xHiNext;
|
||||
$cdfHi = $cdfNext;
|
||||
}
|
||||
|
||||
$dx = 1024;
|
||||
$x = $xNew = 1;
|
||||
$i = 0;
|
||||
@@ -78,17 +105,98 @@ abstract class GammaBase
|
||||
//
|
||||
public static function incompleteGamma(float $a, float $x): float
|
||||
{
|
||||
static $max = 32;
|
||||
$summer = 0;
|
||||
for ($n = 0; $n <= $max; ++$n) {
|
||||
$divisor = $a;
|
||||
for ($i = 1; $i <= $n; ++$i) {
|
||||
$divisor *= ($a + $i);
|
||||
}
|
||||
$summer += ($x ** $n / $divisor);
|
||||
// Unregularized lower incomplete gamma; kept for backward compatibility.
|
||||
return self::regularizedGammaP($a, $x) * self::gammaValue($a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regularized lower incomplete gamma P(a,x) = gamma(a,x) / Gamma(a).
|
||||
* Series for x < a+1, else the complement of the continued fraction.
|
||||
*/
|
||||
public static function regularizedGammaP(float $a, float $x): float
|
||||
{
|
||||
if ($x <= 0.0 || $a <= 0.0) {
|
||||
return 0.0;
|
||||
}
|
||||
if ($x < $a + 1.0) {
|
||||
return self::gammaSeries($a, $x);
|
||||
}
|
||||
|
||||
return $x ** $a * exp(0 - $x) * $summer;
|
||||
return 1.0 - self::gammaContinuedFraction($a, $x);
|
||||
}
|
||||
|
||||
/**
|
||||
* Regularized upper incomplete gamma Q(a,x) = 1 - P(a,x).
|
||||
* Continued fraction for x >= a+1 keeps the right tail free of cancellation.
|
||||
*/
|
||||
public static function regularizedGammaQ(float $a, float $x): float
|
||||
{
|
||||
if ($x <= 0.0 || $a <= 0.0) {
|
||||
return 1.0;
|
||||
}
|
||||
if ($x < $a + 1.0) {
|
||||
return 1.0 - self::gammaSeries($a, $x);
|
||||
}
|
||||
|
||||
return self::gammaContinuedFraction($a, $x);
|
||||
}
|
||||
|
||||
// Near x ~ a both expansions need O(sqrt(a)) terms to reach EPS.
|
||||
private static function incompleteGammaIterations(float $a): int
|
||||
{
|
||||
return max(self::MAX_ITERATIONS, (int) ceil(10.0 * sqrt($a)));
|
||||
}
|
||||
|
||||
// P(a,x) by its series representation (Numerical Recipes gser).
|
||||
private static function gammaSeries(float $a, float $x): float
|
||||
{
|
||||
$maxIterations = self::incompleteGammaIterations($a);
|
||||
$gln = self::logGamma($a);
|
||||
$ap = $a;
|
||||
$sum = 1.0 / $a;
|
||||
$del = $sum;
|
||||
for ($i = 1; $i <= $maxIterations; ++$i) {
|
||||
++$ap;
|
||||
$del *= $x / $ap;
|
||||
$sum += $del;
|
||||
if (abs($del) < abs($sum) * self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $sum * exp(-$x + $a * log($x) - $gln);
|
||||
}
|
||||
|
||||
// Q(a,x) by its continued fraction representation (Numerical Recipes gcf).
|
||||
private static function gammaContinuedFraction(float $a, float $x): float
|
||||
{
|
||||
$maxIterations = self::incompleteGammaIterations($a);
|
||||
$fpMin = 1.0e-300;
|
||||
$gln = self::logGamma($a);
|
||||
$b = $x + 1.0 - $a;
|
||||
$c = 1.0 / $fpMin;
|
||||
$d = 1.0 / $b;
|
||||
$h = $d;
|
||||
for ($i = 1; $i <= $maxIterations; ++$i) {
|
||||
$an = -$i * ($i - $a);
|
||||
$b += 2.0;
|
||||
$d = $an * $d + $b;
|
||||
if (abs($d) < $fpMin) {
|
||||
$d = $fpMin;
|
||||
}
|
||||
$c = $b + $an / $c;
|
||||
if (abs($c) < $fpMin) {
|
||||
$c = $fpMin;
|
||||
}
|
||||
$d = 1.0 / $d;
|
||||
$del = $d * $c;
|
||||
$h *= $del;
|
||||
if (abs($del - 1.0) < self::EPS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $h * exp(-$x + $a * log($x) - $gln);
|
||||
}
|
||||
|
||||
private const GAMMA_VALUE_P0 = 1.000000000190015;
|
||||
|
||||
Vendored
+1
-1
@@ -116,7 +116,7 @@ class Normal
|
||||
// whatever purpose you want, but please show common courtesy and give credit
|
||||
// where credit is due.
|
||||
|
||||
// Input paramater is $p - probability - where 0 < p < 1.
|
||||
// Input parameter is $p - probability - where 0 < p < 1.
|
||||
|
||||
// Coefficients in rational approximations
|
||||
/** @var array<int, float> */
|
||||
|
||||
+225
-41
@@ -4,9 +4,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
|
||||
/**
|
||||
* Some of this code is drived from Perl CPAN Statistical::Distributions.
|
||||
* Its copyright statement is:
|
||||
* Copyright 2003 Michael Kospach. All rights reserved.
|
||||
*
|
||||
* This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
|
||||
*/
|
||||
class StudentT
|
||||
{
|
||||
use ArrayEnabled;
|
||||
@@ -28,9 +34,39 @@ class StudentT
|
||||
* with the same dimensions
|
||||
*/
|
||||
public static function distribution(mixed $value, mixed $degrees, mixed $tails)
|
||||
{
|
||||
return self::calcDistribution($value, $degrees, $tails, self::distribution(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* T.DIST.2T.
|
||||
* Returns the two-tailed Student's t distribution.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotDistDot2T(mixed $value, mixed $degrees)
|
||||
{
|
||||
return self::calcDistribution($value, $degrees, 2, self::distribution(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* T.DIST.RT.
|
||||
* Returns the right-tailed Student's t distribution.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotDistDotRT(mixed $value, mixed $degrees)
|
||||
{
|
||||
return self::calcDistribution($value, $degrees, 1, self::distribution(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
private static function calcDistribution(mixed $value, mixed $degrees, mixed $tails, callable $callback)
|
||||
{
|
||||
if (is_array($value) || is_array($degrees) || is_array($tails)) {
|
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $tails);
|
||||
return self::evaluateArrayArguments($callback, $value, $degrees, $tails);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -45,13 +81,105 @@ class StudentT
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
return self::calculateDistribution($value, $degrees, $tails);
|
||||
return self::subTProb($value, $degrees, $tails);
|
||||
}
|
||||
|
||||
/**
|
||||
* TINV.
|
||||
* Based on code from Perl CPAN Statistical::Distributions.
|
||||
*/
|
||||
private static function subTProb(float $x, int $n, int $tails): float
|
||||
{
|
||||
$w = atan2($x / sqrt($n), 1);
|
||||
$z = cos($w) ** 2;
|
||||
$y = 1;
|
||||
|
||||
for ($i = $n - 2; $i >= 2; $i -= 2) {
|
||||
$y = 1 + ($i - 1) / $i * $z * $y;
|
||||
}
|
||||
|
||||
if ($n % 2 == 0) {
|
||||
$a = sin($w) / 2;
|
||||
$b = 0.5;
|
||||
} else {
|
||||
$a = ($n == 1) ? 0 : (sin($w) * cos($w) / M_PI);
|
||||
$b = 0.5 + $w / M_PI;
|
||||
}
|
||||
|
||||
return $tails * max(0, 1 - $b - $a * $y);
|
||||
}
|
||||
|
||||
/**
|
||||
* T.DIST.
|
||||
* Returns the Student's left-tailed t distribution,
|
||||
* either as a cumulative distribution function (cdf) (TRUE)
|
||||
* or as a probability density function (pdf) (FALSE),
|
||||
* where TRUE/FALSE are the value of $cumulative parameter.
|
||||
*
|
||||
* Returns the one-tailed probability of the chi-squared distribution.
|
||||
* "True" algoritm adapted from java.
|
||||
* org.apache.commons.math3.distribution.TDistribution.
|
||||
* "False" algorithm comes from:
|
||||
* https://statproofbook.github.io/P/t-pdf.html
|
||||
*
|
||||
* @param mixed $cumulative Expecting bool. See above for explanation.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotDist(mixed $value, mixed $degrees, mixed $cumulative)
|
||||
{
|
||||
if (is_array($value) || is_array($degrees) || is_array($cumulative)) {
|
||||
return self::evaluateArrayArguments(self::tDotDist(...), $value, $degrees, $cumulative);
|
||||
}
|
||||
|
||||
try {
|
||||
$value = DistributionValidations::validateFloat($value);
|
||||
$degrees = DistributionValidations::validateInt($degrees);
|
||||
$cumulative = DistributionValidations::validateBool($cumulative);
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
/** @var int $degrees */
|
||||
if (($degrees < 1)) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
/** @var float $value */
|
||||
if (!$cumulative) {
|
||||
return self::tDotDistFalse($value, $degrees);
|
||||
}
|
||||
|
||||
$f16 = $degrees / ($degrees + $value * $value);
|
||||
$g16 = 0.5 * $degrees;
|
||||
$h16 = 0.5;
|
||||
$result = Beta::distribution($f16, $g16, $h16);
|
||||
if (is_numeric($result)) {
|
||||
$result = ($value < 0) ? (0.5 * $result) : (1 - 0.5 * $result);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function tDotDistFalse(float $value, int $degrees): float|string
|
||||
{
|
||||
$result = $k15 = Gamma::gamma(($degrees + 1) / 2);
|
||||
if (is_numeric($k15)) {
|
||||
$result = $k16 = Gamma::gamma($degrees / 2);
|
||||
if (is_numeric($k16)) {
|
||||
$k17 = sqrt(M_PI * $degrees);
|
||||
$k18 = $k15 / ($k16 * $k17);
|
||||
$k19 = $value * $value / $degrees + 1;
|
||||
$k20 = -($degrees + 1) / 2;
|
||||
$k21 = $k19 ** $k20;
|
||||
$result = $k18 * $k21;
|
||||
}
|
||||
}
|
||||
|
||||
/** @var float|string $result */
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* TINV and T.INV.2T.
|
||||
* Returns the two-tailed inverse of the Student t distribution.
|
||||
*
|
||||
* @param mixed $probability Float probability for the function
|
||||
* Or can be an array of values
|
||||
@@ -63,9 +191,17 @@ class StudentT
|
||||
* with the same dimensions
|
||||
*/
|
||||
public static function inverse(mixed $probability, mixed $degrees)
|
||||
{
|
||||
return self::calcInverse($probability, $degrees, 2, self::inverse(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
private static function calcInverse(mixed $probability, mixed $degrees, int $tails, callable $callback2)
|
||||
{
|
||||
if (is_array($probability) || is_array($degrees)) {
|
||||
return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees);
|
||||
return self::evaluateArrayArguments($callback2, $probability, $degrees, $tails);
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -79,54 +215,102 @@ class StudentT
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
|
||||
$callback = fn ($value) => self::distribution($value, $degrees, 2);
|
||||
$callback = fn ($value) => self::distribution($value, $degrees, $tails);
|
||||
|
||||
$newtonRaphson = new NewtonRaphson($callback);
|
||||
|
||||
return $newtonRaphson->execute($probability);
|
||||
$result = $newtonRaphson->execute($probability);
|
||||
if (is_numeric($result) && $tails === 1) {
|
||||
$result = -$result; // @codeCoverageIgnore
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private static function calculateDistribution(float $value, int $degrees, int $tails): float
|
||||
/**
|
||||
* T.INV.
|
||||
* Returns the left-tailed inverse of the Student's t distribution.
|
||||
*
|
||||
* Based on code from Perl CPAN Statistical::Distributions.
|
||||
*
|
||||
* @return array<mixed>|float|string The result, or a string containing an error
|
||||
*/
|
||||
public static function tDotInv(mixed $probability, mixed $degrees)
|
||||
{
|
||||
// tdist, which finds the probability that corresponds to a given value
|
||||
// of t with k degrees of freedom. This algorithm is translated from a
|
||||
// pascal function on p81 of "Statistical Computing in Pascal" by D
|
||||
// Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd:
|
||||
// London). The above Pascal algorithm is itself a translation of the
|
||||
// fortran algoritm "AS 3" by B E Cooper of the Atlas Computer
|
||||
// Laboratory as reported in (among other places) "Applied Statistics
|
||||
// Algorithms", editied by P Griffiths and I D Hill (1985; Ellis
|
||||
// Horwood Ltd.; W. Sussex, England).
|
||||
$tterm = $degrees;
|
||||
$ttheta = atan2($value, sqrt($tterm));
|
||||
$tc = cos($ttheta);
|
||||
$ts = sin($ttheta);
|
||||
|
||||
if (($degrees % 2) === 1) {
|
||||
$ti = 3;
|
||||
$tterm = $tc;
|
||||
} else {
|
||||
$ti = 2;
|
||||
$tterm = 1;
|
||||
if (is_array($probability) || is_array($degrees)) {
|
||||
return self::evaluateArrayArguments(self::tDotInv(...), $probability, $degrees);
|
||||
}
|
||||
|
||||
$tsum = $tterm;
|
||||
while ($ti < $degrees) {
|
||||
$tterm *= $tc * $tc * ($ti - 1) / $ti;
|
||||
$tsum += $tterm;
|
||||
$ti += 2;
|
||||
try {
|
||||
$probability = DistributionValidations::validateProbability($probability);
|
||||
$degrees = DistributionValidations::validateInt($degrees);
|
||||
} catch (Exception $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
$tsum *= $ts;
|
||||
if (($degrees % 2) == 1) {
|
||||
$tsum = Functions::M_2DIVPI * ($tsum + $ttheta);
|
||||
if ($degrees < 1) {
|
||||
return ExcelError::NAN();
|
||||
}
|
||||
if ($probability == 0.5) {
|
||||
return 0.0;
|
||||
}
|
||||
if ($probability < 0.5) {
|
||||
$result = self::tDotInv(1.0 - $probability, $degrees);
|
||||
|
||||
return is_numeric($result) ? -$result : $result;
|
||||
}
|
||||
$p = $probability;
|
||||
$n = $degrees;
|
||||
$u = self::subU($p);
|
||||
$u2 = $u ** 2;
|
||||
|
||||
$a = ($u2 + 1) / 4;
|
||||
$b = ((5 * $u2 + 16) * $u2 + 3) / 96;
|
||||
$c = (((3 * $u2 + 19) * $u2 + 17) * $u2 - 15) / 384;
|
||||
$d = ((((79 * $u2 + 776) * $u2 + 1482) * $u2 - 1920) * $u2 - 945) / 92160;
|
||||
$e = (((((27 * $u2 + 339) * $u2 + 930) * $u2 - 1782) * $u2 - 765) * $u2 + 17955) / 368640;
|
||||
|
||||
$x = $u * (1 + ($a + ($b + ($c + ($d + $e / $n) / $n) / $n) / $n) / $n);
|
||||
|
||||
if ($n <= log10($p) ** 2 + 3) {
|
||||
do {
|
||||
$p1 = self::subTProb($x, $n, 1);
|
||||
$n1 = $n + 1;
|
||||
$delta = ($p1 - $p)
|
||||
/ exp(($n1 * log($n1 / ($n + $x * $x))
|
||||
+ log($n / $n1 / 2 / M_PI) - 1
|
||||
+ (1 / $n1 - 1 / $n) / 6) / 2);
|
||||
$x += $delta;
|
||||
$round = sprintf('%.' . abs((int) (log10(abs($x)) - 4)) . 'F', $delta);
|
||||
} while (($x) && ($round != 0));
|
||||
}
|
||||
|
||||
$tValue = 0.5 * (1 + $tsum);
|
||||
if ($tails == 1) {
|
||||
return 1 - abs($tValue);
|
||||
return -$x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Based on code from Perl CPAN Statistical::Distributions.
|
||||
*/
|
||||
private static function subU(float $p): float
|
||||
{
|
||||
$y = -log(4 * $p * (1 - $p));
|
||||
$x = sqrt(
|
||||
$y * (1.570796288
|
||||
+ $y * (.03706987906
|
||||
+ $y * (-.8364353589E-3
|
||||
+ $y * (-.2250947176E-3
|
||||
+ $y * (.6841218299E-5
|
||||
+ $y * (0.5824238515E-5
|
||||
+ $y * (-.104527497E-5
|
||||
+ $y * (.8360937017E-7
|
||||
+ $y * (-.3231081277E-8
|
||||
+ $y * (.3657763036E-10
|
||||
+ $y * .6936233982E-12))))))))))
|
||||
);
|
||||
if ($p > 0.5) {
|
||||
$x = -$x;
|
||||
}
|
||||
|
||||
return 1 - abs((1 - $tValue) - $tValue);
|
||||
return $x;
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ class Maximum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
@@ -63,7 +63,7 @@ class Maximum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
|
||||
+2
-2
@@ -25,7 +25,7 @@ class Minimum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
@@ -63,7 +63,7 @@ class Minimum extends MaxMinBase
|
||||
// Loop through arguments
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
foreach ($aArgs as $arg) {
|
||||
if (ErrorValue::isError($arg)) {
|
||||
if (ErrorValue::isError($arg, true)) {
|
||||
$returnValue = $arg;
|
||||
|
||||
break;
|
||||
|
||||
+2
@@ -89,6 +89,7 @@ class Percentiles
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
/** @var array<float|int|numeric-string> */
|
||||
$valueSet = self::rankFilterValues($valueSet);
|
||||
$valueCount = count($valueSet);
|
||||
if ($valueCount == 0) {
|
||||
@@ -172,6 +173,7 @@ class Percentiles
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
/** @var array<float|int|numeric-string> */
|
||||
$valueSet = self::rankFilterValues($valueSet);
|
||||
if ($order === self::RANK_SORT_DESCENDING) {
|
||||
rsort($valueSet, SORT_NUMERIC);
|
||||
|
||||
Vendored
+73
-18
@@ -6,11 +6,14 @@ use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class CharacterConvert
|
||||
{
|
||||
use ArrayEnabled;
|
||||
|
||||
private static string $oneByteCharacterSet = 'Windows-1252';
|
||||
|
||||
/**
|
||||
* CHAR.
|
||||
*
|
||||
@@ -27,19 +30,45 @@ class CharacterConvert
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
|
||||
}
|
||||
|
||||
return self::characterBoth($character, true);
|
||||
}
|
||||
|
||||
/** @return array<mixed>|string */
|
||||
public static function characterUnicode(mixed $character): array|string
|
||||
{
|
||||
if (is_array($character)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character);
|
||||
}
|
||||
|
||||
return self::characterBoth($character, false);
|
||||
}
|
||||
|
||||
private static function characterBoth(mixed $character, bool $ansi = true): string
|
||||
{
|
||||
try {
|
||||
$character = Helpers::validateInt($character, true);
|
||||
} catch (CalcExp $e) {
|
||||
return $e->getMessage();
|
||||
}
|
||||
|
||||
if ($ansi && $character === 219 && self::$oneByteCharacterSet[0] === 'M') {
|
||||
return '€';
|
||||
}
|
||||
|
||||
$min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1;
|
||||
if ($character < $min || $character > 255) {
|
||||
if ($character < $min || ($ansi && $character > 255) || $character > 0x10FFFF) {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
$result = iconv('UCS-4LE', 'UTF-8', pack('V', $character));
|
||||
if ($character > 0x10FFFD) { // last assigned
|
||||
return ExcelError::NA();
|
||||
}
|
||||
if ($ansi) {
|
||||
$result = chr($character);
|
||||
|
||||
return ($result === false) ? '' : $result;
|
||||
return (string) iconv(self::$oneByteCharacterSet, 'UTF-8//IGNORE', $result);
|
||||
}
|
||||
|
||||
return mb_chr($character, 'UTF-8');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,7 +86,28 @@ class CharacterConvert
|
||||
if (is_array($characters)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
|
||||
}
|
||||
if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
$characters = $characters ? '1' : '0';
|
||||
}
|
||||
|
||||
return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), true);
|
||||
}
|
||||
|
||||
/** @return array<mixed>|int|string */
|
||||
public static function codeUnicode(mixed $characters): array|string|int
|
||||
{
|
||||
if (is_array($characters)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters);
|
||||
}
|
||||
if (is_bool($characters) && Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
|
||||
$characters = $characters ? '1' : '0';
|
||||
}
|
||||
|
||||
return self::codeBoth(StringHelper::convertToString($characters, convertBool: true), false);
|
||||
}
|
||||
|
||||
private static function codeBoth(string $characters, bool $ansi = true): int|string
|
||||
{
|
||||
try {
|
||||
$characters = Helpers::extractString($characters, true);
|
||||
} catch (CalcExp $e) {
|
||||
@@ -72,22 +122,27 @@ class CharacterConvert
|
||||
if (mb_strlen($characters, 'UTF-8') > 1) {
|
||||
$character = mb_substr($characters, 0, 1, 'UTF-8');
|
||||
}
|
||||
|
||||
return self::unicodeToOrd($character);
|
||||
}
|
||||
|
||||
private static function unicodeToOrd(string $character): int
|
||||
{
|
||||
$retVal = 0;
|
||||
$iconv = iconv('UTF-8', 'UCS-4LE', $character);
|
||||
if ($iconv !== false) {
|
||||
/** @var false|int[] */
|
||||
$result = unpack('V', $iconv);
|
||||
if (is_array($result) && isset($result[1])) {
|
||||
$retVal = $result[1];
|
||||
}
|
||||
if ($ansi && $character === '€' && self::$oneByteCharacterSet[0] === 'M') {
|
||||
return 219;
|
||||
}
|
||||
|
||||
return $retVal;
|
||||
$result = mb_ord($character, 'UTF-8');
|
||||
if ($ansi) {
|
||||
$result = iconv('UTF-8', self::$oneByteCharacterSet . '//IGNORE', $character);
|
||||
|
||||
return ($result !== '') ? ord("$result") : 63; // question mark
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public static function setWindowsCharacterSet(): void
|
||||
{
|
||||
self::$oneByteCharacterSet = 'Windows-1252';
|
||||
}
|
||||
|
||||
public static function setMacCharacterSet(): void
|
||||
{
|
||||
self::$oneByteCharacterSet = 'MAC';
|
||||
}
|
||||
}
|
||||
|
||||
+21
-2
@@ -7,6 +7,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Internal\ExcelArrayPseudoFunctions;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\DataType;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
@@ -17,7 +19,7 @@ class Concatenate
|
||||
/**
|
||||
* This implements the CONCAT function, *not* CONCATENATE.
|
||||
*
|
||||
* @param mixed[] $args
|
||||
* @param mixed $args data to be concatenated
|
||||
*/
|
||||
public static function CONCATENATE(...$args): string
|
||||
{
|
||||
@@ -47,17 +49,33 @@ class Concatenate
|
||||
/**
|
||||
* This implements the CONCATENATE function.
|
||||
*
|
||||
* @param mixed[] $args data to be concatenated
|
||||
* @param mixed $args data to be concatenated
|
||||
*
|
||||
* @return array<string>|string
|
||||
*/
|
||||
public static function actualCONCATENATE(...$args): array|string
|
||||
{
|
||||
$useSingle = false;
|
||||
$cell = null;
|
||||
$count = count($args);
|
||||
if ($args[$count - 1] instanceof Cell) {
|
||||
/** @var Cell */
|
||||
$cell = array_pop($args);
|
||||
$type = $cell->getWorksheet()->getParent()?->getCalculationEngine()->getInstanceArrayReturnType() ?? Calculation::getArrayReturnType();
|
||||
$useSingle = $type === Calculation::RETURN_ARRAY_AS_VALUE;
|
||||
}
|
||||
if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_GNUMERIC) {
|
||||
return self::CONCATENATE(...$args);
|
||||
}
|
||||
$result = '';
|
||||
foreach ($args as $operand2) {
|
||||
if ($useSingle && $cell instanceof Cell && is_array($operand2)) {
|
||||
$temp = Functions::convertArrayToCellRange($operand2);
|
||||
if ($temp !== '') {
|
||||
$operand2 = ExcelArrayPseudoFunctions::single($temp, $cell);
|
||||
}
|
||||
}
|
||||
/** @var null|array<mixed>|bool|float|int|string $operand2 */
|
||||
$result = self::concatenate2Args($result, $operand2);
|
||||
if (ErrorValue::isError($result, true) === true) {
|
||||
break;
|
||||
@@ -142,6 +160,7 @@ class Concatenate
|
||||
$aArgs = Functions::flattenArray($args);
|
||||
$returnValue = self::evaluateTextJoinArray($ignoreEmpty, $aArgs);
|
||||
|
||||
/** @var string[] $aArgs */
|
||||
$returnValue ??= implode($delimiter, $aArgs);
|
||||
if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) {
|
||||
$returnValue = ExcelError::CALC();
|
||||
|
||||
+10
-11
@@ -96,16 +96,13 @@ class Format
|
||||
if ($decimals < 0) {
|
||||
$decimals = 0;
|
||||
}
|
||||
if ($noCommas === false) {
|
||||
$valueResult = number_format(
|
||||
$valueResult,
|
||||
$decimals,
|
||||
StringHelper::getDecimalSeparator(),
|
||||
StringHelper::getThousandsSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
return (string) $valueResult;
|
||||
return number_format(
|
||||
$valueResult,
|
||||
$decimals,
|
||||
StringHelper::getDecimalSeparator(),
|
||||
$noCommas ? '' : StringHelper::getThousandsSeparator()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -209,18 +206,20 @@ class Format
|
||||
Functions::setReturnDateType(Functions::RETURNDATE_EXCEL);
|
||||
|
||||
if (str_contains($value, ':')) {
|
||||
/** @var DateTimeInterface|float|int|string */
|
||||
$timeValue = Functions::scalar(DateTimeExcel\TimeValue::fromString($value));
|
||||
if ($timeValue !== ExcelError::VALUE()) {
|
||||
Functions::setReturnDateType($dateSetting);
|
||||
|
||||
return $timeValue; //* @phpstan-ignore-line
|
||||
return $timeValue;
|
||||
}
|
||||
}
|
||||
/** @var DateTimeInterface|float|int|string */
|
||||
$dateValue = Functions::scalar(DateTimeExcel\DateValue::fromString($value));
|
||||
if ($dateValue !== ExcelError::VALUE()) {
|
||||
Functions::setReturnDateType($dateSetting);
|
||||
|
||||
return $dateValue; //* @phpstan-ignore-line
|
||||
return $dateValue;
|
||||
}
|
||||
Functions::setReturnDateType($dateSetting);
|
||||
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ class Search
|
||||
use ArrayEnabled;
|
||||
|
||||
/**
|
||||
* FIND (case sensitive search).
|
||||
* FIND (case-sensitive search).
|
||||
*
|
||||
* @param mixed $needle The string to look for
|
||||
* Or can be an array of values
|
||||
@@ -54,7 +54,7 @@ class Search
|
||||
}
|
||||
|
||||
/**
|
||||
* SEARCH (case insensitive search).
|
||||
* SEARCH (case-insensitive search).
|
||||
*
|
||||
* @param mixed $needle The string to look for
|
||||
* Or can be an array of values
|
||||
|
||||
+2
-2
@@ -171,12 +171,12 @@ class Text
|
||||
{
|
||||
$columnCount = array_reduce(
|
||||
$rows,
|
||||
fn (int $counter, array $row): int => max($counter, count($row)), //* @phpstan-ignore-line
|
||||
fn (int $counter, array $row): int => max($counter, count($row)), //* @phpstan-ignore argument.type (I don't know)
|
||||
0
|
||||
);
|
||||
|
||||
return array_map(
|
||||
fn (array $row): array => (count($row) < $columnCount) //* @phpstan-ignore-line
|
||||
fn (array $row): array => (count($row) < $columnCount) //* @phpstan-ignore argument.type (I don't know)
|
||||
? array_merge($row, array_fill(0, $columnCount - count($row), $padding))
|
||||
: $row,
|
||||
$rows
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\TextData;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
|
||||
class Thai
|
||||
{
|
||||
use ArrayEnabled;
|
||||
|
||||
private const THAI_DIGITS = [
|
||||
0 => 'ศูนย์',
|
||||
1 => 'หนึ่ง',
|
||||
2 => 'สอง',
|
||||
3 => 'สาม',
|
||||
4 => 'สี่',
|
||||
5 => 'ห้า',
|
||||
6 => 'หก',
|
||||
7 => 'เจ็ด',
|
||||
8 => 'แปด',
|
||||
9 => 'เก้า',
|
||||
];
|
||||
|
||||
private const THAI_UNITS = [
|
||||
1 => 'สิบ',
|
||||
2 => 'ร้อย',
|
||||
3 => 'พัน',
|
||||
4 => 'หมื่น',
|
||||
5 => 'แสน',
|
||||
6 => 'ล้าน',
|
||||
];
|
||||
|
||||
private const THAI_COMPOUND_ONE = 'เอ็ด';
|
||||
private const THAI_COMPOUND_TWO = 'ยี่';
|
||||
private const THAI_INTEGER = 'ถ้วน';
|
||||
private const THAI_MINUS = 'ลบ';
|
||||
private const THAI_BAHT = 'บาท';
|
||||
private const THAI_SATANG = 'สตางค์';
|
||||
|
||||
/**
|
||||
* BAHTTEXT.
|
||||
*
|
||||
* @param mixed $number The number or array of numbers to convert
|
||||
*
|
||||
* @return array<mixed>|string If an array of values is passed as the argument, then the returned result will also be an array with the same dimensions
|
||||
*/
|
||||
public static function getBahtText(mixed $number): array|string
|
||||
{
|
||||
if (is_array($number)) {
|
||||
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number);
|
||||
}
|
||||
|
||||
if (is_string($number) && preg_match('/^-?\d+$/', $number)) {
|
||||
$isNegative = str_starts_with($number, '-');
|
||||
$baht = ltrim($number, '-0') ?: '0';
|
||||
$satang = '00';
|
||||
} elseif (is_bool($number) || is_numeric($number)) {
|
||||
$number += 0;
|
||||
$isNegative = $number < 0;
|
||||
[$baht, $satang] = explode('.', number_format(abs($number), 2, '.', ''));
|
||||
} else {
|
||||
return ExcelError::VALUE();
|
||||
}
|
||||
|
||||
$hasWhole = $baht !== '0';
|
||||
$hasFraction = $satang !== '00';
|
||||
|
||||
if (!$hasWhole && !$hasFraction) {
|
||||
return self::THAI_DIGITS[0] . self::THAI_BAHT . self::THAI_INTEGER;
|
||||
}
|
||||
|
||||
$text = $isNegative
|
||||
? self::THAI_MINUS
|
||||
: '';
|
||||
|
||||
if ($hasWhole) {
|
||||
$text .= self::convertLarge($baht) . self::THAI_BAHT;
|
||||
}
|
||||
|
||||
$text .= $hasFraction
|
||||
? self::convertBlock($satang) . self::THAI_SATANG
|
||||
: self::THAI_INTEGER;
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private static function convertLarge(string $digits): string
|
||||
{
|
||||
$length = strlen($digits) % 6 ?: 6;
|
||||
|
||||
$chunks = [
|
||||
substr($digits, 0, $length),
|
||||
...str_split(substr($digits, $length), 6),
|
||||
];
|
||||
|
||||
$chunks = array_filter($chunks, fn (string $chunk): bool => $chunk !== '');
|
||||
|
||||
return implode(
|
||||
self::THAI_UNITS[6],
|
||||
array_map(self::convertBlock(...), $chunks)
|
||||
);
|
||||
}
|
||||
|
||||
private static function convertBlock(string $block): string
|
||||
{
|
||||
$out = '';
|
||||
$length = strlen($block);
|
||||
$i = 0;
|
||||
|
||||
// Hundreds and higher powers
|
||||
for ($power = $length - 1; $power >= 2; --$power) {
|
||||
$digit = $block[$i++];
|
||||
if ($digit !== '0') {
|
||||
$out .= self::THAI_DIGITS[$digit] . self::THAI_UNITS[$power];
|
||||
}
|
||||
}
|
||||
|
||||
// Tens
|
||||
$ten = $length > 1 ? $block[$i++] : '0';
|
||||
if ($ten !== '0') {
|
||||
$out .= match ($ten) {
|
||||
'1' => '',
|
||||
'2' => self::THAI_COMPOUND_TWO,
|
||||
default => self::THAI_DIGITS[$ten],
|
||||
} . self::THAI_UNITS[1];
|
||||
}
|
||||
|
||||
// Ones
|
||||
$one = $block[$i] ?? '0';
|
||||
if ($one !== '0') {
|
||||
$out .= $ten !== '0' && $one === '1'
|
||||
? self::THAI_COMPOUND_ONE
|
||||
: self::THAI_DIGITS[$one];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
+29
-23
@@ -2,9 +2,10 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\Web;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
|
||||
use PhpOffice\PhpSpreadsheet\Settings;
|
||||
use Psr\Http\Client\ClientExceptionInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class Service
|
||||
{
|
||||
@@ -18,38 +19,43 @@ class Service
|
||||
*
|
||||
* @return string the output resulting from a call to the webservice
|
||||
*/
|
||||
public static function webService(string $url): string
|
||||
public static function webService(mixed $url, ?Cell $cell = null): ?string
|
||||
{
|
||||
$url = trim($url);
|
||||
if (strlen($url) > 2048) {
|
||||
if (is_array($url)) {
|
||||
$url = Functions::flattenSingleValue($url);
|
||||
}
|
||||
$url = trim(StringHelper::convertToString($url, false));
|
||||
if (mb_strlen($url) > 2048) {
|
||||
return ExcelError::VALUE(); // Invalid URL length
|
||||
}
|
||||
|
||||
if (!preg_match('/^http[s]?:\/\//', $url)) {
|
||||
$parsed = parse_url($url);
|
||||
$scheme = $parsed['scheme'] ?? '';
|
||||
if ($scheme !== 'http' && $scheme !== 'https') {
|
||||
return ExcelError::VALUE(); // Invalid protocol
|
||||
}
|
||||
$domainWhiteList = $cell?->getWorksheet()->getParent()?->getDomainWhiteList() ?? [];
|
||||
$host = $parsed['host'] ?? '';
|
||||
if (!in_array($host, $domainWhiteList, true)) {
|
||||
return ($cell === null) ? null : Functions::NOT_YET_IMPLEMENTED; // will be converted to oldCalculatedValue or null
|
||||
}
|
||||
|
||||
// Get results from the webservice
|
||||
$client = Settings::getHttpClient();
|
||||
$requestFactory = Settings::getRequestFactory();
|
||||
$request = $requestFactory->createRequest('GET', $url);
|
||||
|
||||
try {
|
||||
$response = $client->sendRequest($request);
|
||||
} catch (ClientExceptionInterface) {
|
||||
return ExcelError::VALUE(); // cURL error
|
||||
$ctxArray = [
|
||||
'http' => [
|
||||
'follow_location' => 0,
|
||||
'user_agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
|
||||
],
|
||||
];
|
||||
if ($scheme === 'https') {
|
||||
$ctxArray['ssl'] = ['crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT];
|
||||
}
|
||||
|
||||
if ($response->getStatusCode() != 200) {
|
||||
return ExcelError::VALUE(); // cURL error
|
||||
}
|
||||
|
||||
$output = $response->getBody()->getContents();
|
||||
if (strlen($output) > 32767) {
|
||||
$ctx = stream_context_create($ctxArray);
|
||||
$output = @file_get_contents($url, false, $ctx);
|
||||
if ($output === false || mb_strlen($output) > 32767) {
|
||||
return ExcelError::VALUE(); // Output not a string or too long
|
||||
}
|
||||
|
||||
return $output;
|
||||
return ($output === '') ? Functions::NOT_YET_IMPLEMENTED : $output;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
BIN
Binary file not shown.
+1
@@ -38,6 +38,7 @@ DVARP = DBVARP
|
||||
## Datum- en tijdfuncties (Date & Time Functions)
|
||||
##
|
||||
DATE = DATUM
|
||||
DATEDIF = DATUMVERSCHIL
|
||||
DATESTRING = DATUMNOTATIE
|
||||
DATEVALUE = DATUMWAARDE
|
||||
DAY = DAG
|
||||
|
||||
@@ -58,7 +58,7 @@ class AddressHelper
|
||||
$columnReference = (string) $currentColumnNumber;
|
||||
}
|
||||
// Bracketed C references are relative to the current column
|
||||
if (is_string($columnReference) && $columnReference[0] === '[') { // @phpstan-ignore-line
|
||||
if ($columnReference[0] === '[') {
|
||||
$columnReference = $currentColumnNumber + (int) trim($columnReference, '[]');
|
||||
}
|
||||
$columnReference = (int) $columnReference;
|
||||
|
||||
@@ -7,11 +7,14 @@ namespace PhpOffice\PhpSpreadsheet\Cell;
|
||||
*/
|
||||
interface AddressRange
|
||||
{
|
||||
public const MAX_ROW = 1048576;
|
||||
|
||||
public const MAX_ROW = 1_048_576;
|
||||
public const MAX_COLUMN = 'XFD';
|
||||
public const MAX_COLUMN_INT = 16_384;
|
||||
|
||||
public const MAX_COLUMN_INT = 16384;
|
||||
public const MAX_ROW_XLS_OLD = 16_384;
|
||||
public const MAX_ROW_XLS = 65_536;
|
||||
public const MAX_COLUMN_XLS = 'IV';
|
||||
public const MAX_COLUMN_INT_XLS = 256;
|
||||
|
||||
/**
|
||||
* @return T
|
||||
|
||||
+1
-1
@@ -60,7 +60,7 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder
|
||||
// Check for currency
|
||||
if (preg_match(FormattedNumber::currencyMatcherRegexp(), (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value), $matches, PREG_UNMATCHED_AS_NULL)) {
|
||||
// Convert value to number
|
||||
$sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null;
|
||||
$sign = ($matches['PrefixedSign'] ?? $matches['PrefixedSign2'] ?? $matches['PostfixedSign']) ?? null; //* @phpstan-ignore nullCoalesce.unnecessary (Not sure whether Phpstan is correct)
|
||||
$currencyCode = $matches['PrefixedCurrency'] ?? $matches['PostfixedCurrency'] ?? '';
|
||||
/** @var string */
|
||||
$temp = str_replace([$decimalSeparatorNoPreg, $currencyCode, ' ', '-'], ['.', '', '', ''], (string) preg_replace('/(\d)' . $thousandsSeparator . '(\d)/u', '$1$2', $value));
|
||||
|
||||
+170
-18
@@ -15,6 +15,7 @@ use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Protection;
|
||||
use PhpOffice\PhpSpreadsheet\Style\Style;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Table;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use Stringable;
|
||||
@@ -190,12 +191,22 @@ class Cell implements Stringable
|
||||
public function getFormattedValue(): string
|
||||
{
|
||||
$currentCalendar = SharedDate::getExcelCalendar();
|
||||
SharedDate::setExcelCalendar($this->getWorksheet()->getParent()?->getExcelCalendar());
|
||||
$formattedValue = (string) NumberFormat::toFormattedString(
|
||||
$this->getCalculatedValueString(),
|
||||
(string) $this->getStyle()->getNumberFormat()->getFormatCode(true)
|
||||
SharedDate::setExcelCalendar(
|
||||
$this->getWorksheet()
|
||||
->getParent()
|
||||
?->getExcelCalendar()
|
||||
);
|
||||
SharedDate::setExcelCalendar($currentCalendar);
|
||||
|
||||
try {
|
||||
$formattedValue = NumberFormat::toFormattedString(
|
||||
$this->getCalculatedValueString(),
|
||||
(string) $this->getStyle()
|
||||
->getNumberFormat()
|
||||
->getFormatCode(true)
|
||||
);
|
||||
} finally {
|
||||
SharedDate::setExcelCalendar($currentCalendar);
|
||||
}
|
||||
|
||||
return $formattedValue;
|
||||
}
|
||||
@@ -233,6 +244,9 @@ class Cell implements Stringable
|
||||
*/
|
||||
public function setValue(mixed $value, ?IValueBinder $binder = null): self
|
||||
{
|
||||
if ($this->hadHyperlink) {
|
||||
$this->clearHyperlink();
|
||||
}
|
||||
// Cells?->Worksheet?->Spreadsheet
|
||||
$binder ??= $this->parent?->getParent()?->getParent()?->getValueBinder() ?? self::getValueBinder();
|
||||
if (!$binder->bindValue($this, $value)) {
|
||||
@@ -242,11 +256,31 @@ class Cell implements Stringable
|
||||
return $this;
|
||||
}
|
||||
|
||||
private bool $hadHyperlink = false;
|
||||
|
||||
/** @internal */
|
||||
public function setHadHyperlink(bool $hadHyperlink): void
|
||||
{
|
||||
$this->hadHyperlink = $hadHyperlink;
|
||||
}
|
||||
|
||||
private function clearHyperlink(): void
|
||||
{
|
||||
$worksheet = $this->getWorksheetOrNull();
|
||||
if ($worksheet !== null) {
|
||||
$coordinate = $this->getCoordinate();
|
||||
$worksheet->setHyperlink($coordinate, null);
|
||||
}
|
||||
$this->hadHyperlink = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the value for a cell, with the explicit data type passed to the method (bypassing any use of the value binder).
|
||||
*
|
||||
* @param mixed $value Value
|
||||
* @param string $dataType Explicit data type, see DataType::TYPE_*
|
||||
* This parameter is currently optional (default = string).
|
||||
* Omitting it is ***DEPRECATED***, and the default will be removed in a future release.
|
||||
* Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this
|
||||
* method, then it is your responsibility as an end-user developer to validate that the value and
|
||||
* the datatype match.
|
||||
@@ -257,6 +291,9 @@ class Cell implements Stringable
|
||||
*/
|
||||
public function setValueExplicit(mixed $value, string $dataType = DataType::TYPE_STRING): self
|
||||
{
|
||||
if ($this->hadHyperlink) {
|
||||
$this->clearHyperlink();
|
||||
}
|
||||
$oldValue = $this->value;
|
||||
$quotePrefix = false;
|
||||
|
||||
@@ -307,6 +344,14 @@ class Cell implements Stringable
|
||||
$this->value = SharedDate::convertIsoDate($value);
|
||||
$dataType = DataType::TYPE_NUMERIC;
|
||||
|
||||
break;
|
||||
case DataType::TYPE_DRAWING_IN_CELL:
|
||||
if ($value instanceof BaseDrawing) {
|
||||
$this->value = $value;
|
||||
} else {
|
||||
throw new SpreadsheetException('Item is not a drawing');
|
||||
}
|
||||
|
||||
break;
|
||||
case DataType::TYPE_ERROR:
|
||||
$this->value = DataType::checkErrorCode($value);
|
||||
@@ -325,16 +370,16 @@ class Cell implements Stringable
|
||||
$worksheet = $this->getWorksheet();
|
||||
$spreadsheet = $worksheet->getParent();
|
||||
if (isset($spreadsheet) && $spreadsheet->getIndex($worksheet, true) >= 0) {
|
||||
$originalSelected = $worksheet->getSelectedCells();
|
||||
$activeSheetIndex = $spreadsheet->getActiveSheetIndex();
|
||||
$style = $this->getStyle();
|
||||
$oldQuotePrefix = $style->getQuotePrefix();
|
||||
// Avoid Worksheet::getStyle() (selection + validation) unless quotePrefix must change.
|
||||
$oldQuotePrefix = $spreadsheet->getCellXfByIndex($this->getXfIndex())->getQuotePrefix();
|
||||
if ($oldQuotePrefix !== $quotePrefix) {
|
||||
$style->setQuotePrefix($quotePrefix);
|
||||
}
|
||||
$worksheet->setSelectedCells($originalSelected);
|
||||
if ($activeSheetIndex >= 0) {
|
||||
$spreadsheet->setActiveSheetIndex($activeSheetIndex);
|
||||
$originalSelected = $worksheet->getSelectedCells();
|
||||
$activeSheetIndex = $spreadsheet->getActiveSheetIndex();
|
||||
$this->getStyle()->setQuotePrefix($quotePrefix);
|
||||
$worksheet->setSelectedCells($originalSelected);
|
||||
if ($activeSheetIndex >= 0) {
|
||||
$spreadsheet->setActiveSheetIndex($activeSheetIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,7 +436,7 @@ class Cell implements Stringable
|
||||
$value = array_shift($value);
|
||||
}
|
||||
|
||||
return StringHelper::convertToString($value, false);
|
||||
return StringHelper::convertToString($value, false, convertBool: true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -503,7 +548,7 @@ class Cell implements Stringable
|
||||
$coordinate = $this->getCoordinate();
|
||||
$ref = $oldAttributesRef;
|
||||
if (preg_match('/^([A-Z]{1,3})([0-9]{1,7})(:([A-Z]{1,3})([0-9]{1,7}))?$/', $ref, $matches) === 1) {
|
||||
if (isset($matches[3])) {
|
||||
if (isset($matches[5])) {
|
||||
$minCol = $matches[1];
|
||||
$minRow = (int) $matches[2];
|
||||
$maxCol = $matches[4];
|
||||
@@ -746,7 +791,8 @@ class Cell implements Stringable
|
||||
throw new SpreadsheetException('Cannot get hyperlink for cell that is not bound to a worksheet');
|
||||
}
|
||||
|
||||
return $this->getWorksheet()->getHyperlink($this->getCoordinate());
|
||||
return $this->getWorksheet()
|
||||
->getHyperlink($this->getCoordinate());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -760,7 +806,8 @@ class Cell implements Stringable
|
||||
throw new SpreadsheetException('Cannot set hyperlink for cell that is not bound to a worksheet');
|
||||
}
|
||||
|
||||
$this->getWorksheet()->setHyperlink($this->getCoordinate(), $hyperlink);
|
||||
$this->getWorksheet()
|
||||
->setHyperlink($this->getCoordinate(), $hyperlink);
|
||||
|
||||
return $this->updateInCollection();
|
||||
}
|
||||
@@ -973,6 +1020,19 @@ class Cell implements Stringable
|
||||
return $this->updateInCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the XF index without triggering updateInCollection().
|
||||
*
|
||||
* This is intended for use by readers that will immediately follow with
|
||||
* setValueExplicit(), avoiding a redundant cache write.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
public function setXfIndexNoUpdate(int $indexValue): void
|
||||
{
|
||||
$this->xfIndex = $indexValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the formula attributes.
|
||||
*
|
||||
@@ -1034,4 +1094,96 @@ class Cell implements Stringable
|
||||
|
||||
return $hidden !== Protection::PROTECTION_UNPROTECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $right positions to the right of this one.
|
||||
*/
|
||||
public function cursorRight(int $right = 1): self
|
||||
{
|
||||
$row = $this->getRow();
|
||||
$col = $this->getColumn();
|
||||
$colIndex = Coordinate::columnIndexFromString($col);
|
||||
$newCol = max(
|
||||
1,
|
||||
min($colIndex + $right, AddressRange::MAX_COLUMN_INT)
|
||||
);
|
||||
$newColStr = Coordinate::stringFromColumnIndex($newCol);
|
||||
|
||||
return $this->getWorksheet()->getCell("$newColStr$row");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $left positions to the left of this one.
|
||||
*/
|
||||
public function cursorLeft(int $left = 1): self
|
||||
{
|
||||
return $this->cursorRight(-$left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $down positions below this one.
|
||||
*/
|
||||
public function cursorDown(int $down = 1): self
|
||||
{
|
||||
$row = $this->getRow();
|
||||
$col = $this->getColumn();
|
||||
$newRow = max(
|
||||
1,
|
||||
min($row + $down, AddressRange::MAX_ROW)
|
||||
);
|
||||
|
||||
return $this->getWorksheet()->getCell("$col$newRow");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell $up positions above this one.
|
||||
*/
|
||||
public function cursorUp(int $up = 1): self
|
||||
{
|
||||
return $this->cursorDown(-$up);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell at row $row in current column.
|
||||
*/
|
||||
public function cursorRow(int $row = 1): self
|
||||
{
|
||||
$col = $this->getColumn();
|
||||
$newRow = max(
|
||||
1,
|
||||
min($row, AddressRange::MAX_ROW)
|
||||
);
|
||||
|
||||
return $this->getWorksheet()->getCell("$col$newRow");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell at column $column in current row.
|
||||
*/
|
||||
public function cursorColumn(string $column = 'A'): self
|
||||
{
|
||||
$row = $this->getRow();
|
||||
$colIndex = Coordinate::columnIndexFromString($column);
|
||||
$newCol = max(
|
||||
1,
|
||||
min($colIndex, AddressRange::MAX_COLUMN_INT)
|
||||
);
|
||||
$newColStr = Coordinate::stringFromColumnIndex($newCol);
|
||||
|
||||
return $this->getWorksheet()->getCell("$newColStr$row");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return cell adjusted for Xls limits if applicable.
|
||||
*/
|
||||
public function cursorXlsLimits(): self
|
||||
{
|
||||
$row = min($this->getRow(), AddressRange::MAX_ROW_XLS);
|
||||
$column = $this->getColumn();
|
||||
$colIndex = Coordinate::columnIndexFromString($column);
|
||||
$newCol = min($colIndex, AddressRange::MAX_COLUMN_INT_XLS);
|
||||
$newColStr = Coordinate::stringFromColumnIndex($newCol);
|
||||
|
||||
return $this->getWorksheet()->getCell("$newColStr$row");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,11 @@ abstract class Coordinate
|
||||
public static function coordinateFromString(string $cellAddress): array
|
||||
{
|
||||
if (preg_match(self::A1_COORDINATE_REGEX, $cellAddress, $matches)) {
|
||||
return [$matches['col'], $matches['row']];
|
||||
$row = (int) ltrim($matches['row'], '$');
|
||||
// reluctantly allow row 0 due to regression problems
|
||||
if (/*$row > 0 &&*/ $row <= AddressRange::MAX_ROW) {
|
||||
return [$matches['col'], $matches['row']];
|
||||
}
|
||||
} elseif (self::coordinateIsRange($cellAddress)) {
|
||||
throw new Exception('Cell coordinate string can not be a range of cells');
|
||||
} elseif ($cellAddress == '') {
|
||||
@@ -186,7 +190,7 @@ abstract class Coordinate
|
||||
/**
|
||||
* Build range from coordinate strings.
|
||||
*
|
||||
* @param mixed[] $range Array containing one or more arrays containing one or two coordinate strings
|
||||
* @param array<array<string>> $range Array containing one or more arrays containing one or two coordinate strings
|
||||
*
|
||||
* @return string String representation of $pRange
|
||||
*/
|
||||
@@ -206,6 +210,7 @@ abstract class Coordinate
|
||||
$range[$i] = implode(':', $range[$i]);
|
||||
}
|
||||
|
||||
/** @var array<string> $range */
|
||||
return implode(',', $range);
|
||||
}
|
||||
|
||||
@@ -316,7 +321,7 @@ abstract class Coordinate
|
||||
|
||||
$worksheet = $matches['worksheet'];
|
||||
if ($worksheet !== '') {
|
||||
if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") {
|
||||
if (str_starts_with($worksheet, "'") && str_ends_with($worksheet, "'")) {
|
||||
$worksheet = substr($worksheet, 1, -1);
|
||||
}
|
||||
$data['worksheet'] = strtolower($worksheet);
|
||||
@@ -401,7 +406,7 @@ abstract class Coordinate
|
||||
return $indexCache[$columnAddress];
|
||||
}
|
||||
// It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array
|
||||
// rather than use ord() and make it case insensitive to get rid of the strtoupper() as well.
|
||||
// rather than use ord() and make it case-insensitive to get rid of the strtoupper() as well.
|
||||
// Because it's a static, there's no significant memory overhead either.
|
||||
/** @var array<string, int> */
|
||||
static $columnLookup = [
|
||||
@@ -420,22 +425,28 @@ abstract class Coordinate
|
||||
$indexCache[$columnAddress] = $columnLookup[$columnAddress];
|
||||
|
||||
return $indexCache[$columnAddress];
|
||||
} elseif (!isset($columnAddress[2])) {
|
||||
}
|
||||
if (!isset($columnAddress[2])) {
|
||||
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26
|
||||
+ $columnLookup[$columnAddress[1]];
|
||||
|
||||
return $indexCache[$columnAddress];
|
||||
} elseif (!isset($columnAddress[3])) {
|
||||
$indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676
|
||||
}
|
||||
if (!isset($columnAddress[3])) {
|
||||
$temp = $columnLookup[$columnAddress[0]] * 676
|
||||
+ $columnLookup[$columnAddress[1]] * 26
|
||||
+ $columnLookup[$columnAddress[2]];
|
||||
|
||||
return $indexCache[$columnAddress];
|
||||
if ($temp <= AddressRange::MAX_COLUMN_INT) {
|
||||
$indexCache[$columnAddress] = $temp;
|
||||
|
||||
return $temp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception(
|
||||
'Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty')
|
||||
'Column string index can not be ' . ((isset($columnAddress[0])) ? ('beyond ' . AddressRange::MAX_COLUMN) : 'empty')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -446,11 +457,19 @@ abstract class Coordinate
|
||||
*
|
||||
* @param int|numeric-string $columnIndex Column index (A = 1)
|
||||
*/
|
||||
public static function stringFromColumnIndex(int|string $columnIndex): string
|
||||
public static function stringFromColumnIndex(int|string $columnIndex, bool $tolerateZero = false): string
|
||||
{
|
||||
/** @var string[] */
|
||||
static $indexCache = [];
|
||||
$columnIndex2 = (int) $columnIndex;
|
||||
if ($columnIndex2 === 0 && $tolerateZero) {
|
||||
return '';
|
||||
}
|
||||
if ($columnIndex2 < 1 || $columnIndex2 > AddressRange::MAX_COLUMN_INT) {
|
||||
throw new Exception("Invalid column index $columnIndex");
|
||||
}
|
||||
|
||||
$columnIndex = $columnIndex2;
|
||||
if (!isset($indexCache[$columnIndex])) {
|
||||
$indexValue = $columnIndex;
|
||||
$base26 = '';
|
||||
@@ -503,8 +522,7 @@ abstract class Coordinate
|
||||
}
|
||||
|
||||
/** @var string[] */
|
||||
$cellList = array_merge(...$cells); //* @phpstan-ignore-line
|
||||
// Unsure how to satisfy phpstan in line above
|
||||
$cellList = array_merge(...$cells); //* @phpstan-ignore argument.type (Unsure how to satisfy phpstan)
|
||||
|
||||
$retVal = array_map(
|
||||
fn (string $cellAddress) => ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress,
|
||||
@@ -516,7 +534,7 @@ abstract class Coordinate
|
||||
|
||||
/**
|
||||
* @param mixed[] $operators
|
||||
* @param mixed[][] $cells
|
||||
* @param string[][] $cells
|
||||
*
|
||||
* @return mixed[]
|
||||
*/
|
||||
@@ -554,7 +572,7 @@ abstract class Coordinate
|
||||
$row = 0;
|
||||
sscanf($coordinate, '%[A-Z]%d', $column, $row);
|
||||
/** @var int $row */
|
||||
$key = (--$row * 16384) + self::columnIndexFromString((string) $column);
|
||||
$key = (--$row * AddressRange::MAX_COLUMN_INT) + self::columnIndexFromString((string) $column);
|
||||
$sortKeys[$key] = $coordinate;
|
||||
}
|
||||
ksort($sortKeys);
|
||||
@@ -628,10 +646,8 @@ abstract class Coordinate
|
||||
|
||||
// Range...
|
||||
[$rangeStart, $rangeEnd] = $range;
|
||||
[$startColumn, $startRow] = self::coordinateFromString($rangeStart);
|
||||
[$endColumn, $endRow] = self::coordinateFromString($rangeEnd);
|
||||
$startColumnIndex = self::columnIndexFromString($startColumn);
|
||||
$endColumnIndex = self::columnIndexFromString($endColumn);
|
||||
[$startColumnIndex, $startRow, $startColumn] = self::indexesFromString($rangeStart);
|
||||
[$endColumnIndex, $endRow, $endColumn] = self::indexesFromString($rangeEnd);
|
||||
++$endColumnIndex;
|
||||
|
||||
// Current data
|
||||
@@ -642,8 +658,6 @@ abstract class Coordinate
|
||||
|
||||
// Loop cells
|
||||
while ($currentColumnIndex < $endColumnIndex) {
|
||||
/** @var int $currentRow */
|
||||
/** @var int $endRow */
|
||||
while ($currentRow <= $endRow) {
|
||||
$returnValue[] = self::stringFromColumnIndex($currentColumnIndex) . $currentRow;
|
||||
++$currentRow;
|
||||
@@ -671,7 +685,7 @@ abstract class Coordinate
|
||||
*
|
||||
* @param array<string, mixed> $coordinateCollection associative array mapping coordinates to values
|
||||
*
|
||||
* @return array<string, mixed> associative array mapping coordinate ranges to valuea
|
||||
* @return array<string, mixed> associative array mapping coordinate ranges to values
|
||||
*/
|
||||
public static function mergeRangesInCollection(array $coordinateCollection): array
|
||||
{
|
||||
@@ -726,7 +740,7 @@ abstract class Coordinate
|
||||
}
|
||||
}
|
||||
|
||||
if ($rowStart !== null) { // @phpstan-ignore-line
|
||||
if ($rowStart !== null) {
|
||||
if ($rowStart == $rowEnd) {
|
||||
$ranges[] = $hashedValue->col . $rowStart;
|
||||
} else {
|
||||
|
||||
@@ -17,6 +17,7 @@ class DataType
|
||||
const TYPE_INLINE = 'inlineStr';
|
||||
const TYPE_ERROR = 'e';
|
||||
const TYPE_ISO_DATE = 'd';
|
||||
const TYPE_DRAWING_IN_CELL = 'drawingCell';
|
||||
|
||||
/**
|
||||
* List of error codes.
|
||||
|
||||
+12
-4
@@ -4,11 +4,12 @@ namespace PhpOffice\PhpSpreadsheet\Cell;
|
||||
|
||||
use Composer\Pcre\Preg;
|
||||
use DateTimeInterface;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\CalculationParserOnly;
|
||||
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as SpreadsheetException;
|
||||
use PhpOffice\PhpSpreadsheet\RichText\RichText;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
|
||||
use Stringable;
|
||||
|
||||
class DefaultValueBinder implements IValueBinder
|
||||
@@ -33,6 +34,11 @@ class DefaultValueBinder implements IValueBinder
|
||||
$value = $value->format('Y-m-d H:i:s');
|
||||
} elseif ($value instanceof Stringable) {
|
||||
$value = (string) $value;
|
||||
} elseif ($value instanceof BaseDrawing) {
|
||||
$value->setCoordinates($cell->getCoordinate());
|
||||
$value->setResizeProportional(false);
|
||||
$value->setInCell(true);
|
||||
$value->setWorksheet($cell->getWorksheet(), true);
|
||||
} else {
|
||||
throw new SpreadsheetException('Unable to bind unstringable ' . gettype($value));
|
||||
}
|
||||
@@ -68,6 +74,9 @@ class DefaultValueBinder implements IValueBinder
|
||||
if ($value instanceof RichText) {
|
||||
return DataType::TYPE_INLINE;
|
||||
}
|
||||
if ($value instanceof BaseDrawing) {
|
||||
return DataType::TYPE_DRAWING_IN_CELL;
|
||||
}
|
||||
if ($value instanceof Stringable) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
@@ -77,8 +86,7 @@ class DefaultValueBinder implements IValueBinder
|
||||
throw new SpreadsheetException("unusable type $gettype");
|
||||
}
|
||||
if (strlen($value) > 1 && $value[0] === '=') {
|
||||
$calculation = new Calculation();
|
||||
$calculation->disableBranchPruning();
|
||||
$calculation = CalculationParserOnly::getParserInstance();
|
||||
|
||||
try {
|
||||
if (empty($calculation->parseFormula($value))) {
|
||||
@@ -107,7 +115,7 @@ class DefaultValueBinder implements IValueBinder
|
||||
return DataType::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
if (!is_numeric($value)) {
|
||||
if (!is_numeric($value) || !is_finite((float) $value)) {
|
||||
return DataType::TYPE_STRING;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ class Hyperlink
|
||||
*/
|
||||
private string $tooltip;
|
||||
|
||||
private string $display = '';
|
||||
|
||||
/**
|
||||
* Create a new Hyperlink.
|
||||
*
|
||||
@@ -80,6 +82,23 @@ class Hyperlink
|
||||
return $this->isInternal() ? '' : 'External';
|
||||
}
|
||||
|
||||
public function getDisplay(): string
|
||||
{
|
||||
return $this->display;
|
||||
}
|
||||
|
||||
/**
|
||||
* This can be displayed in cell rather than actual cell contents.
|
||||
* It seems to be ignored by Excel.
|
||||
* It may be used by Google Sheets.
|
||||
*/
|
||||
public function setDisplay(string $display): self
|
||||
{
|
||||
$this->display = $display;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get hash code.
|
||||
*
|
||||
@@ -89,7 +108,11 @@ class Hyperlink
|
||||
{
|
||||
return md5(
|
||||
$this->url
|
||||
. ','
|
||||
. $this->tooltip
|
||||
. ','
|
||||
. $this->display
|
||||
. ','
|
||||
. __CLASS__
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ class IgnoredErrors
|
||||
|
||||
private bool $evalError = false;
|
||||
|
||||
private bool $misleadingFormat = false;
|
||||
|
||||
public function setNumberStoredAsText(bool $value): self
|
||||
{
|
||||
$this->numberStoredAsText = $value;
|
||||
@@ -73,4 +75,16 @@ class IgnoredErrors
|
||||
{
|
||||
return $this->evalError;
|
||||
}
|
||||
|
||||
public function setMisleadingFormat(bool $value): self
|
||||
{
|
||||
$this->misleadingFormat = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMisleadingFormat(): bool
|
||||
{
|
||||
return $this->misleadingFormat;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -336,7 +336,7 @@ class Axis extends Properties
|
||||
{
|
||||
parent::__clone();
|
||||
$this->majorGridlines = ($this->majorGridlines === null) ? null : clone $this->majorGridlines;
|
||||
$this->majorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines;
|
||||
$this->minorGridlines = ($this->minorGridlines === null) ? null : clone $this->minorGridlines;
|
||||
$this->axisText = ($this->axisText === null) ? null : clone $this->axisText;
|
||||
$this->dispUnitsTitle = ($this->dispUnitsTitle === null) ? null : clone $this->dispUnitsTitle;
|
||||
$this->fillColor = clone $this->fillColor;
|
||||
|
||||
@@ -110,6 +110,37 @@ class Chart
|
||||
|
||||
private bool $roundedCorners = false;
|
||||
|
||||
private bool $date1904 = false;
|
||||
|
||||
private string $lang = 'en-GB';
|
||||
|
||||
/** @var array{
|
||||
* b?: numeric-string,
|
||||
* l?: numeric-string,
|
||||
* r?: numeric-string,
|
||||
* t?: numeric-string,
|
||||
* header?: numeric-string,
|
||||
* footer?: numeric-string,
|
||||
* }
|
||||
*/
|
||||
private array $pageMargins = [
|
||||
'b' => '0.75',
|
||||
'l' => '0.7',
|
||||
'r' => '0.7',
|
||||
't' => '0.75',
|
||||
'header' => '0.3',
|
||||
'footer' => '0.3',
|
||||
];
|
||||
|
||||
/** @var array{
|
||||
* paperSize?: string,
|
||||
* orientation?: string,
|
||||
* }
|
||||
*/
|
||||
private array $pageSetup = [
|
||||
'orientation' => 'portrait',
|
||||
];
|
||||
|
||||
private GridLines $borderLines;
|
||||
|
||||
private ChartColor $fillColor;
|
||||
@@ -680,9 +711,11 @@ class Chart
|
||||
return $this->autoTitleDeleted;
|
||||
}
|
||||
|
||||
public function setAutoTitleDeleted(bool $autoTitleDeleted): self
|
||||
public function setAutoTitleDeleted(?bool $autoTitleDeleted): self
|
||||
{
|
||||
$this->autoTitleDeleted = $autoTitleDeleted;
|
||||
if (is_bool($autoTitleDeleted)) {
|
||||
$this->autoTitleDeleted = $autoTitleDeleted;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
@@ -725,6 +758,34 @@ class Chart
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDate1904(): bool
|
||||
{
|
||||
return $this->date1904;
|
||||
}
|
||||
|
||||
public function setDate1904(?bool $date1904): self
|
||||
{
|
||||
if ($date1904 !== null) {
|
||||
$this->date1904 = $date1904;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLang(): string
|
||||
{
|
||||
return $this->lang;
|
||||
}
|
||||
|
||||
public function setLang(?string $lang): self
|
||||
{
|
||||
if ($lang !== null && $lang !== '') {
|
||||
$this->lang = $lang;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getBorderLines(): GridLines
|
||||
{
|
||||
return $this->borderLines;
|
||||
@@ -782,4 +843,58 @@ class Chart
|
||||
$this->borderLines = clone $this->borderLines;
|
||||
$this->fillColor = clone $this->fillColor;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* b?: numeric-string,
|
||||
* l?: numeric-string,
|
||||
* r?: numeric-string,
|
||||
* t?: numeric-string,
|
||||
* header?: numeric-string,
|
||||
* footer?: numeric-string,
|
||||
* }
|
||||
*/
|
||||
public function getPageMargins(): array
|
||||
{
|
||||
return $this->pageMargins;
|
||||
}
|
||||
|
||||
/** @param mixed $pageMargins expecting array matching $this->pageMargins */
|
||||
public function setPageMargins(mixed $pageMargins): self
|
||||
{
|
||||
if (is_array($pageMargins)) {
|
||||
foreach (['b', 'l', 'r', 't', 'header', 'footer'] as $key) {
|
||||
$value = $pageMargins[$key] ?? null;
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
$this->pageMargins[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* paperSize?: string,
|
||||
* orientation?: string,
|
||||
* }
|
||||
*/
|
||||
public function getPageSetup(): array
|
||||
{
|
||||
return $this->pageSetup;
|
||||
}
|
||||
|
||||
/** @param mixed $pageSetup expecting array matching $this->pageSetup */
|
||||
public function setPageSetup(mixed $pageSetup): self
|
||||
{
|
||||
if (is_array($pageSetup)) {
|
||||
foreach (['paperSize', 'orientation'] as $key) {
|
||||
$value = $pageSetup[$key] ?? null;
|
||||
if (is_string($value)) {
|
||||
$this->pageSetup[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ class ChartColor
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @param array{value: ?string, alpha: null|int|string, brightness?: null|int|string, type: ?string} $color */
|
||||
/** @param array{value: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: null|string} $color */
|
||||
public function setColorPropertiesArray(array $color): self
|
||||
{
|
||||
return $this->setColorProperties(
|
||||
|
||||
@@ -387,7 +387,7 @@ class DataSeries
|
||||
$plotLabels = $this->plotLabel;
|
||||
$this->plotLabel = [];
|
||||
foreach ($plotLabels as $plotLabel) {
|
||||
$this->plotLabel[] = $plotLabel;
|
||||
$this->plotLabel[] = clone $plotLabel;
|
||||
}
|
||||
$plotCategories = $this->plotCategory;
|
||||
$this->plotCategory = [];
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Chart;
|
||||
|
||||
class DataTable
|
||||
{
|
||||
private bool $showHorizontalBorder = true;
|
||||
|
||||
private bool $showVerticalBorder = true;
|
||||
|
||||
private bool $showOutline = true;
|
||||
|
||||
private bool $showKeys = true;
|
||||
|
||||
public function getShowHorizontalBorder(): bool
|
||||
{
|
||||
return $this->showHorizontalBorder;
|
||||
}
|
||||
|
||||
public function getShowVerticalBorder(): bool
|
||||
{
|
||||
return $this->showVerticalBorder;
|
||||
}
|
||||
|
||||
public function getShowOutline(): bool
|
||||
{
|
||||
return $this->showOutline;
|
||||
}
|
||||
|
||||
public function getShowKeys(): bool
|
||||
{
|
||||
return $this->showKeys;
|
||||
}
|
||||
|
||||
public function setShowHorizontalBorder(bool $showHorizontalBorder): self
|
||||
{
|
||||
$this->showHorizontalBorder = $showHorizontalBorder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setShowVerticalBorder(bool $showVerticalBorder): self
|
||||
{
|
||||
$this->showVerticalBorder = $showVerticalBorder;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setShowOutline(bool $showOutline): self
|
||||
{
|
||||
$this->showOutline = $showOutline;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setShowKeys(bool $showKeys): self
|
||||
{
|
||||
$this->showKeys = $showKeys;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -99,6 +99,26 @@ class Layout
|
||||
|
||||
private ?Properties $labelEffects = null;
|
||||
|
||||
/** @var array{
|
||||
* vertOverflow?: string,
|
||||
* horzOverflow?: string,
|
||||
* wrap?: string,
|
||||
* lIns?: numeric-string,
|
||||
* tIns?: numeric-string,
|
||||
* rIns?: numeric-string,
|
||||
* bIns?: numeric-string,
|
||||
* anchor?: string,
|
||||
* }
|
||||
*/
|
||||
private array $bodyPr = [
|
||||
'wrap' => 'square',
|
||||
'lIns' => '38100',
|
||||
'tIns' => '19050',
|
||||
'rIns' => '38100',
|
||||
'bIns' => '19050',
|
||||
'anchor' => 'ctr',
|
||||
];
|
||||
|
||||
/**
|
||||
* Create a new Layout.
|
||||
*
|
||||
@@ -106,7 +126,22 @@ class Layout
|
||||
*/
|
||||
public function __construct(array $layout = [])
|
||||
{
|
||||
/** @var array{layoutTarget?: string, xMode?: string, yMode?: string, x?: float, y?: float, w?:float, h?:float, dLblPos?: string, labelFont?: ?mixed, labelFontColor?: ?mixed, labelEffects?: ?mixed, numFmtCode?: string} $layout */
|
||||
/** @var array{
|
||||
* layoutTarget?: string,
|
||||
* xMode?: string,
|
||||
* yMode?: string,
|
||||
* x?: float,
|
||||
* y?: float,
|
||||
* w?:float,
|
||||
* h?:float,
|
||||
* dLblPos?: string,
|
||||
* labelFont?: ?mixed,
|
||||
* labelFontColor?: ?mixed,
|
||||
* labelEffects?: ?mixed,
|
||||
* numFmtCode?: string,
|
||||
* bodyPr?: mixed,
|
||||
* } $layout
|
||||
*/
|
||||
if (isset($layout['layoutTarget'])) {
|
||||
$this->layoutTarget = $layout['layoutTarget'];
|
||||
}
|
||||
@@ -156,6 +191,10 @@ class Layout
|
||||
if ($labelEffects instanceof Properties) {
|
||||
$this->labelEffects = $labelEffects;
|
||||
}
|
||||
$bodyPr = $layout['bodyPr'] ?? null;
|
||||
if (is_array($bodyPr)) {
|
||||
$this->setBodyPr($bodyPr);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param mixed[] $layout */
|
||||
@@ -469,11 +508,7 @@ class Layout
|
||||
|
||||
public function getLabelFontColor(): ?ChartColor
|
||||
{
|
||||
if ($this->labelFont === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->labelFont->getChartColor();
|
||||
return $this->labelFont?->getChartColor();
|
||||
}
|
||||
|
||||
public function setLabelFontColor(?ChartColor $chartColor): self
|
||||
@@ -523,6 +558,45 @@ class Layout
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return array{
|
||||
* vertOverflow?: string,
|
||||
* horzOverflow?: string,
|
||||
* wrap?: string,
|
||||
* lIns?: numeric-string,
|
||||
* tIns?: numeric-string,
|
||||
* rIns?: numeric-string,
|
||||
* bIns?: numeric-string,
|
||||
* anchor?: string,
|
||||
* }
|
||||
*/
|
||||
public function getBodyPr(): array
|
||||
{
|
||||
return $this->bodyPr;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $bodyPr expect array matching $this->bodyPr
|
||||
*/
|
||||
public function setBodyPr(mixed $bodyPr): self
|
||||
{
|
||||
if (is_array($bodyPr)) {
|
||||
foreach (['vertOverflow', 'horzOverflow', 'wrap', 'anchor'] as $key) {
|
||||
$value = $bodyPr[$key] ?? null;
|
||||
if (is_string($value)) {
|
||||
$this->bodyPr[$key] = "$value";
|
||||
}
|
||||
}
|
||||
foreach (['lIns', 'tIns', 'rIns', 'bIns'] as $key) {
|
||||
$value = $bodyPr[$key] ?? null;
|
||||
if (is_string($value) && is_numeric($value)) {
|
||||
$this->bodyPr[$key] = "$value";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement PHP __clone to create a deep clone, not just a shallow copy.
|
||||
*/
|
||||
|
||||
@@ -66,7 +66,7 @@ class Legend
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legend position as an excel string value.
|
||||
* Get legend position as an Excel string value.
|
||||
*/
|
||||
public function getPosition(): string
|
||||
{
|
||||
@@ -74,7 +74,7 @@ class Legend
|
||||
}
|
||||
|
||||
/**
|
||||
* Get legend position using an excel string value.
|
||||
* Get legend position using an Excel string value.
|
||||
*
|
||||
* @param string $position see self::POSITION_*
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,8 @@ class PlotArea
|
||||
*/
|
||||
private array $plotSeries;
|
||||
|
||||
private ?DataTable $dataTable = null;
|
||||
|
||||
/**
|
||||
* Create a new PlotArea.
|
||||
*
|
||||
@@ -195,16 +197,34 @@ class PlotArea
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function setDataTable(DataTable $dataTable): self
|
||||
{
|
||||
$this->dataTable = $dataTable;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDataTable(): ?DataTable
|
||||
{
|
||||
return $this->dataTable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement PHP __clone to create a deep clone, not just a shallow copy.
|
||||
*/
|
||||
public function __clone()
|
||||
{
|
||||
$this->layout = ($this->layout === null) ? null : clone $this->layout;
|
||||
$this->dataTable = ($this->dataTable === null) ? null : clone $this->dataTable;
|
||||
$plotSeries = $this->plotSeries;
|
||||
$this->plotSeries = [];
|
||||
foreach ($plotSeries as $series) {
|
||||
$this->plotSeries[] = clone $series;
|
||||
}
|
||||
$gradientFillStops = $this->gradientFillStops;
|
||||
$this->gradientFillStops = [];
|
||||
foreach ($gradientFillStops as $gradientFillStop) {
|
||||
$this->gradientFillStops[] = [$gradientFillStop[0], clone $gradientFillStop[1]];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,9 +99,11 @@ abstract class Properties
|
||||
const SHADOW_PRESETS_PERSPECTIVE_LOWER_RIGHT = 22;
|
||||
const SHADOW_PRESETS_PERSPECTIVE_LOWER_LEFT = 23;
|
||||
|
||||
const POINTS_WIDTH_MULTIPLIER = 12700;
|
||||
const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky
|
||||
const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy
|
||||
const POINTS_WIDTH_MULTIPLIER = 12_700;
|
||||
const ANGLE_MULTIPLIER = 60_000; // direction and size-kx size-ky
|
||||
const PERCENTAGE_MULTIPLIER = 100_000; // size sx and sy, and gradient pos
|
||||
const MAX_SKEW_ANGLE_XML = 90 * self::ANGLE_MULTIPLIER - 1;
|
||||
const MAX_SKEW_ANGLE_DEGREES = self::MAX_SKEW_ANGLE_XML / self::ANGLE_MULTIPLIER; // max for size-kx size-ky
|
||||
|
||||
protected bool $objectState = false; // used only for minor gridlines
|
||||
|
||||
@@ -417,7 +419,7 @@ abstract class Properties
|
||||
}
|
||||
|
||||
foreach ($elements as $keys) {
|
||||
$reference = &$reference[$keys]; //* @phpstan-ignore-line
|
||||
$reference = &$reference[$keys]; //* @phpstan-ignore offsetAccess.nonOffsetAccessible (I'm not sure what this is doing)
|
||||
}
|
||||
|
||||
return $reference;
|
||||
@@ -519,7 +521,7 @@ abstract class Properties
|
||||
return $this->softEdges['size'];
|
||||
}
|
||||
|
||||
/** @param null|array{value?: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: ?string}|float|string $value */
|
||||
/** @param null|array{value?: ?string, alpha?: null|int|string, brightness?: null|int|string, type?: ?string, sx?: ?float, sy?: ?float, ky?: ?float, kx?: ?float}|float|string $value */
|
||||
public function setShadowProperty(string $propertyName, mixed $value): self
|
||||
{
|
||||
$this->activateObject();
|
||||
@@ -853,7 +855,7 @@ abstract class Properties
|
||||
public function setLineStyleProperty(string $propertyName, mixed $value): self
|
||||
{
|
||||
$this->activateObject();
|
||||
$this->lineStyleProperties[$propertyName] = $value; //* @phpstan-ignore-line
|
||||
$this->lineStyleProperties[$propertyName] = $value; //* @phpstan-ignore assign.propertyType (Too complicated)
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@
|
||||
namespace PhpOffice\PhpSpreadsheet\Chart\Renderer;
|
||||
|
||||
/**
|
||||
* Jpgraph is not oficially maintained in Composer, so the version there
|
||||
* Jpgraph is not officially maintained in Composer, so the version there
|
||||
* could be out of date. For that reason, all unit test requiring Jpgraph
|
||||
* are skipped. So, do not measure code coverage for this class till that
|
||||
* is fixed.
|
||||
|
||||
+1
-1
@@ -81,7 +81,7 @@ abstract class JpGraphRendererBase implements IRenderer
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be overriden in descendants to do real JpGraph library initialization.
|
||||
* This method should be overridden in descendants to do real JpGraph library initialization.
|
||||
*/
|
||||
abstract protected static function init(): void;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Collection;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Cell\AddressRange;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Cell;
|
||||
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
|
||||
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
|
||||
@@ -11,7 +12,8 @@ use Psr\SimpleCache\CacheInterface;
|
||||
|
||||
class Cells
|
||||
{
|
||||
protected const MAX_COLUMN_ID = 16384;
|
||||
/** @deprecated 5.6.0 use AddressRange::MAX_COLUMN_INT */
|
||||
protected const MAX_COLUMN_ID = AddressRange::MAX_COLUMN_INT;
|
||||
|
||||
private CacheInterface $cache;
|
||||
|
||||
@@ -242,9 +244,9 @@ class Cells
|
||||
// Lookup highest column and highest row
|
||||
$maxRow = $maxColumn = 1;
|
||||
foreach ($this->index as $coordinate) {
|
||||
$row = (int) floor(($coordinate - 1) / self::MAX_COLUMN_ID) + 1;
|
||||
$row = (int) floor(($coordinate - 1) / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$maxRow = ($maxRow > $row) ? $maxRow : $row;
|
||||
$column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID;
|
||||
$column = ($coordinate % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
|
||||
$maxColumn = ($maxColumn > $column) ? $maxColumn : $column;
|
||||
}
|
||||
|
||||
@@ -274,14 +276,14 @@ class Cells
|
||||
}
|
||||
|
||||
$maxColumn = 1;
|
||||
$toRow = $row * self::MAX_COLUMN_ID;
|
||||
$fromRow = --$row * self::MAX_COLUMN_ID;
|
||||
$toRow = $row * AddressRange::MAX_COLUMN_INT;
|
||||
$fromRow = --$row * AddressRange::MAX_COLUMN_INT;
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate < $fromRow || $coordinate >= $toRow) {
|
||||
continue;
|
||||
}
|
||||
$column = ($coordinate % self::MAX_COLUMN_ID) ?: self::MAX_COLUMN_ID;
|
||||
$maxColumn = $maxColumn > $column ? $maxColumn : $column;
|
||||
$column = ($coordinate % AddressRange::MAX_COLUMN_INT) ?: AddressRange::MAX_COLUMN_INT;
|
||||
$maxColumn = max($column, $maxColumn);
|
||||
}
|
||||
|
||||
return Coordinate::stringFromColumnIndex($maxColumn);
|
||||
@@ -304,10 +306,10 @@ class Cells
|
||||
$maxRow = 1;
|
||||
$columnIndex = Coordinate::columnIndexFromString($column);
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate % self::MAX_COLUMN_ID !== $columnIndex) {
|
||||
if ($coordinate % AddressRange::MAX_COLUMN_INT !== $columnIndex) {
|
||||
continue;
|
||||
}
|
||||
$row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
|
||||
$row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$maxRow = ($maxRow > $row) ? $maxRow : $row;
|
||||
}
|
||||
|
||||
@@ -371,12 +373,12 @@ class Cells
|
||||
throw new PhpSpreadsheetException('Row number must be a positive integer');
|
||||
}
|
||||
|
||||
$toRow = $row * self::MAX_COLUMN_ID;
|
||||
$fromRow = --$row * self::MAX_COLUMN_ID;
|
||||
$toRow = $row * AddressRange::MAX_COLUMN_INT;
|
||||
$fromRow = --$row * AddressRange::MAX_COLUMN_INT;
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate >= $fromRow && $coordinate < $toRow) {
|
||||
$row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID);
|
||||
$row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % AddressRange::MAX_COLUMN_INT);
|
||||
$this->delete("{$column}{$row}");
|
||||
}
|
||||
}
|
||||
@@ -393,9 +395,9 @@ class Cells
|
||||
|
||||
$columnIndex = Coordinate::columnIndexFromString($column);
|
||||
foreach ($this->index as $coordinate) {
|
||||
if ($coordinate % self::MAX_COLUMN_ID === $columnIndex) {
|
||||
$row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID);
|
||||
if ($coordinate % AddressRange::MAX_COLUMN_INT === $columnIndex) {
|
||||
$row = (int) floor($coordinate / AddressRange::MAX_COLUMN_INT) + 1;
|
||||
$column = Coordinate::stringFromColumnIndex($coordinate % AddressRange::MAX_COLUMN_INT);
|
||||
$this->delete("{$column}{$row}");
|
||||
}
|
||||
}
|
||||
@@ -443,7 +445,7 @@ class Cells
|
||||
$row = '';
|
||||
sscanf($cellCoordinate, '%[A-Z]%d', $column, $row);
|
||||
/** @var int $row */
|
||||
$this->index[$cellCoordinate] = (--$row * self::MAX_COLUMN_ID) + Coordinate::columnIndexFromString((string) $column);
|
||||
$this->index[$cellCoordinate] = (--$row * AddressRange::MAX_COLUMN_INT) + Coordinate::columnIndexFromString((string) $column);
|
||||
|
||||
// Clear index sorted flag and index caches
|
||||
$this->indexSorted = false;
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Helper;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use DateTimeZone;
|
||||
use PhpOffice\PhpSpreadsheet\Exception;
|
||||
|
||||
/**
|
||||
@@ -29,19 +31,16 @@ 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 is not accessible');
|
||||
}
|
||||
$filepath = "{$folder}/{$filename}";
|
||||
$this->filepath = (string) realpath($filepath);
|
||||
$this->filename = basename($filepath);
|
||||
clearstatcache();
|
||||
if ((is_file($this->filepath) === false) || (is_readable($this->filepath) === false)) {
|
||||
throw new Exception('File not found, or not a regular file, or cannot be read');
|
||||
$filepath = realpath("{$folder}/{$filename}");
|
||||
if ($filepath === false || !is_file($filepath) || !is_readable($filepath)) {
|
||||
throw new Exception('File not found, or cannot be read');
|
||||
}
|
||||
$this->filepath = $filepath;
|
||||
$this->filename = basename($this->filepath);
|
||||
|
||||
$filetype ??= pathinfo($filename, PATHINFO_EXTENSION);
|
||||
if (array_key_exists(strtolower($filetype), self::CONTENT_TYPES) === false) {
|
||||
$filetype ??= pathinfo($this->filename, PATHINFO_EXTENSION);
|
||||
if (!array_key_exists(strtolower($filetype), self::CONTENT_TYPES)) {
|
||||
throw new Exception('Invalid filetype: file cannot be downloaded');
|
||||
}
|
||||
$this->filetype = strtolower($filetype);
|
||||
@@ -90,7 +89,8 @@ class Downloader
|
||||
|
||||
// If you're serving to IE over SSL, then the following may be needed
|
||||
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); // Date in the past
|
||||
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); // always modified
|
||||
$dt = new DateTimeImmutable(timezone: new DateTimeZone('UTC'));
|
||||
header('Last-Modified: ' . $dt->format('D, d M Y H:i:s') . ' GMT'); // always modified
|
||||
header('Cache-Control: cache, must-revalidate'); // HTTP/1.1
|
||||
header('Pragma: public'); // HTTP/1.0
|
||||
}
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Helper;
|
||||
|
||||
class Handler
|
||||
/** @internal */
|
||||
final class Handler
|
||||
{
|
||||
private static string $invalidHex = 'Y';
|
||||
|
||||
// A bunch of methods to show that we continue
|
||||
// to capture messages even using PhpUnit 10.
|
||||
public static function suppressed(): bool
|
||||
public static function suppressed(): true
|
||||
{
|
||||
return @trigger_error('hello');
|
||||
}
|
||||
@@ -28,17 +29,17 @@ class Handler
|
||||
return file_get_contents(__FILE__ . 'noexist') !== false;
|
||||
}
|
||||
|
||||
public static function userDeprecated(): bool
|
||||
public static function userDeprecated(): true
|
||||
{
|
||||
return trigger_error('hello', E_USER_DEPRECATED);
|
||||
}
|
||||
|
||||
public static function userNotice(): bool
|
||||
public static function userNotice(): true
|
||||
{
|
||||
return trigger_error('userNotice', E_USER_NOTICE);
|
||||
}
|
||||
|
||||
public static function userWarning(): bool
|
||||
public static function userWarning(): true
|
||||
{
|
||||
return trigger_error('userWarning', E_USER_WARNING);
|
||||
}
|
||||
|
||||
@@ -131,16 +131,21 @@ class Sample
|
||||
$callStartTime = microtime(true);
|
||||
$writer->save($path);
|
||||
$this->logWrite($writer, $path, $callStartTime);
|
||||
if ($this->isCli() === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
echo '<a href="/download.php?type=' . pathinfo($path, PATHINFO_EXTENSION) . '&name=' . basename($path) . '">Download ' . basename($path) . '</a><br />';
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
$this->addDownloadLink($path);
|
||||
}
|
||||
|
||||
$this->logEndingNotes();
|
||||
}
|
||||
|
||||
public function addDownloadLink(string $path): void
|
||||
{
|
||||
if ($this->isCli() === false) {
|
||||
// @codeCoverageIgnoreStart
|
||||
echo '<a href="/download.php?type=' . pathinfo($path, PATHINFO_EXTENSION) . '&name=' . basename($path) . '">Download ' . basename($path) . '</a><br />';
|
||||
// @codeCoverageIgnoreEnd
|
||||
}
|
||||
}
|
||||
|
||||
protected function isDirOrMkdir(string $folder): bool
|
||||
{
|
||||
return \is_dir($folder) || \mkdir($folder);
|
||||
@@ -242,9 +247,15 @@ class Sample
|
||||
}
|
||||
|
||||
/** @param mixed[][] $matrix */
|
||||
public function displayGrid(array $matrix): void
|
||||
public function displayGrid(array $matrix, null|bool|TextGridRightAlign $numbersRight = null): void
|
||||
{
|
||||
$renderer = new TextGrid($matrix, $this->isCli());
|
||||
if (is_bool($numbersRight)) {
|
||||
$numbersRight = $numbersRight ? TextGridRightAlign::numeric : TextGridRightAlign::none;
|
||||
}
|
||||
if ($numbersRight !== null) {
|
||||
$renderer->setNumbersRight($numbersRight);
|
||||
}
|
||||
echo $renderer->render();
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
|
||||
|
||||
class TextGrid
|
||||
{
|
||||
private bool $isCli;
|
||||
protected bool $isCli;
|
||||
|
||||
/** @var mixed[][] */
|
||||
protected array $matrix;
|
||||
@@ -17,16 +17,18 @@ class TextGrid
|
||||
/** @var string[] */
|
||||
protected array $columns;
|
||||
|
||||
private string $gridDisplay;
|
||||
protected string $gridDisplay;
|
||||
|
||||
private bool $rowDividers = false;
|
||||
protected bool $rowDividers = false;
|
||||
|
||||
private bool $rowHeaders = true;
|
||||
protected bool $rowHeaders = true;
|
||||
|
||||
private bool $columnHeaders = true;
|
||||
protected bool $columnHeaders = true;
|
||||
|
||||
protected TextGridRightAlign $numbersRight = TextGridRightAlign::none;
|
||||
|
||||
/** @param mixed[][] $matrix */
|
||||
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true)
|
||||
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true, TextGridRightAlign $numbersRight = TextGridRightAlign::none)
|
||||
{
|
||||
$this->rows = array_keys($matrix);
|
||||
$this->columns = array_keys($matrix[$this->rows[0]]);
|
||||
@@ -44,6 +46,12 @@ class TextGrid
|
||||
$this->rowDividers = $rowDividers;
|
||||
$this->rowHeaders = $rowHeaders;
|
||||
$this->columnHeaders = $columnHeaders;
|
||||
$this->numbersRight = $numbersRight;
|
||||
}
|
||||
|
||||
public function setNumbersRight(TextGridRightAlign $numbersRight): void
|
||||
{
|
||||
$this->numbersRight = $numbersRight;
|
||||
}
|
||||
|
||||
public function render(): string
|
||||
@@ -52,7 +60,7 @@ class TextGrid
|
||||
|
||||
if (!empty($this->rows)) {
|
||||
$maxRow = max($this->rows);
|
||||
$maxRowLength = strlen((string) $maxRow) + 1;
|
||||
$maxRowLength = $this->strlen((string) $maxRow) + 1;
|
||||
$columnWidths = $this->getColumnWidths();
|
||||
|
||||
$this->renderColumnHeader($maxRowLength, $columnWidths);
|
||||
@@ -68,7 +76,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @param int[] $columnWidths */
|
||||
private function renderRows(int $maxRowLength, array $columnWidths): void
|
||||
protected function renderRows(int $maxRowLength, array $columnWidths): void
|
||||
{
|
||||
foreach ($this->matrix as $row => $rowData) {
|
||||
if ($this->rowHeaders) {
|
||||
@@ -86,18 +94,27 @@ class TextGrid
|
||||
* @param mixed[] $rowData
|
||||
* @param int[] $columnWidths
|
||||
*/
|
||||
private function renderCells(array $rowData, array $columnWidths): void
|
||||
protected function renderCells(array $rowData, array $columnWidths): void
|
||||
{
|
||||
foreach ($rowData as $column => $cell) {
|
||||
$valueForLength = $this->getString($cell);
|
||||
$displayCell = $this->isCli ? $valueForLength : htmlentities($valueForLength);
|
||||
$this->gridDisplay .= '| ';
|
||||
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
|
||||
if ($this->rightAlign($displayCell, $cell)) {
|
||||
$this->gridDisplay .= str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength)) . $displayCell . ' ';
|
||||
} else {
|
||||
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected function rightAlign(string $displayCell, mixed $cell = null): bool
|
||||
{
|
||||
return ($this->numbersRight === TextGridRightAlign::numeric && is_numeric($displayCell)) || ($this->numbersRight === TextGridRightAlign::floatOrInt && (is_int($cell) || is_float($cell)));
|
||||
}
|
||||
|
||||
/** @param int[] $columnWidths */
|
||||
private function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
|
||||
protected function renderColumnHeader(int $maxRowLength, array &$columnWidths): void
|
||||
{
|
||||
if (!$this->columnHeaders) {
|
||||
$this->renderFooter($maxRowLength, $columnWidths);
|
||||
@@ -129,7 +146,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @param int[] $columnWidths */
|
||||
private function renderFooter(int $maxRowLength, array $columnWidths): void
|
||||
protected function renderFooter(int $maxRowLength, array $columnWidths): void
|
||||
{
|
||||
if ($this->rowHeaders) {
|
||||
$this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1);
|
||||
@@ -142,7 +159,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @return int[] */
|
||||
private function getColumnWidths(): array
|
||||
protected function getColumnWidths(): array
|
||||
{
|
||||
$columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix);
|
||||
$columnWidths = [];
|
||||
@@ -154,7 +171,7 @@ class TextGrid
|
||||
}
|
||||
|
||||
/** @param mixed[] $columnData */
|
||||
private function getColumnWidth(array $columnData): int
|
||||
protected function getColumnWidth(array $columnData): int
|
||||
{
|
||||
$columnWidth = 0;
|
||||
$columnData = array_values($columnData);
|
||||
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Helper;
|
||||
|
||||
enum TextGridRightAlign
|
||||
{
|
||||
case none;
|
||||
case numeric;
|
||||
case floatOrInt;
|
||||
}
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Reader\IReader;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\CsvNoEscape;
|
||||
use PhpOffice\PhpSpreadsheet\Reader\IReader2 as IReader;
|
||||
use PhpOffice\PhpSpreadsheet\Shared\File;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\IWriter;
|
||||
|
||||
@@ -24,6 +25,8 @@ abstract class IOFactory
|
||||
public const READER_HTML = 'Html';
|
||||
public const READER_CSV = 'Csv';
|
||||
|
||||
public const USE_CSV_NO_ESCAPE = [self::READER_CSV => CsvNoEscape::class];
|
||||
|
||||
public const WRITER_XLSX = 'Xlsx';
|
||||
public const WRITER_XLS = 'Xls';
|
||||
public const WRITER_ODS = 'Ods';
|
||||
@@ -54,6 +57,31 @@ abstract class IOFactory
|
||||
'Mpdf' => Writer\Pdf\Mpdf::class,
|
||||
];
|
||||
|
||||
/** @internal */
|
||||
public static function restoreDefaultReadersAndWriters(): void
|
||||
{
|
||||
self::$readers = [
|
||||
self::READER_XLSX => Reader\Xlsx::class,
|
||||
self::READER_XLS => Reader\Xls::class,
|
||||
self::READER_XML => Reader\Xml::class,
|
||||
self::READER_ODS => Reader\Ods::class,
|
||||
self::READER_SLK => Reader\Slk::class,
|
||||
self::READER_GNUMERIC => Reader\Gnumeric::class,
|
||||
self::READER_HTML => Reader\Html::class,
|
||||
self::READER_CSV => Reader\Csv::class,
|
||||
];
|
||||
self::$writers = [
|
||||
self::WRITER_XLS => Writer\Xls::class,
|
||||
self::WRITER_XLSX => Writer\Xlsx::class,
|
||||
self::WRITER_ODS => Writer\Ods::class,
|
||||
self::WRITER_CSV => Writer\Csv::class,
|
||||
self::WRITER_HTML => Writer\Html::class,
|
||||
'Tcpdf' => Writer\Pdf\Tcpdf::class,
|
||||
'Dompdf' => Writer\Pdf\Dompdf::class,
|
||||
'Mpdf' => Writer\Pdf\Mpdf::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Writer\IWriter.
|
||||
*/
|
||||
@@ -75,18 +103,22 @@ abstract class IOFactory
|
||||
|
||||
/**
|
||||
* Create IReader.
|
||||
*
|
||||
* @param array<string, class-string<IReader>> $mergeArray
|
||||
* Array to use to find reader, self::$readers will be used if $readers is empty.
|
||||
*/
|
||||
public static function createReader(string $readerType): IReader
|
||||
public static function createReader(string $readerType, array $mergeArray = []): IReader
|
||||
{
|
||||
/** @var class-string<IReader> */
|
||||
$className = $readerType;
|
||||
if (!in_array($readerType, self::$readers, true)) {
|
||||
if (!isset(self::$readers[$readerType])) {
|
||||
$readers = empty($mergeArray) ? self::$readers : $mergeArray;
|
||||
if (!in_array($readerType, $readers, true)) {
|
||||
if (!isset($readers[$readerType])) {
|
||||
throw new Reader\Exception("No reader found for type $readerType");
|
||||
}
|
||||
|
||||
// Instantiate reader
|
||||
$className = self::$readers[$readerType];
|
||||
$className = $readers[$readerType];
|
||||
}
|
||||
|
||||
return new $className();
|
||||
@@ -106,10 +138,11 @@ abstract class IOFactory
|
||||
* list of Readers so it will only try the subset that you specify here.
|
||||
* Values in this list can be any of the constant values defined in the set
|
||||
* IOFactory::READER_*.
|
||||
* @param array<string, class-string<IReader>> $mergeArray
|
||||
*/
|
||||
public static function load(string $filename, int $flags = 0, ?array $readers = null): Spreadsheet
|
||||
public static function load(string $filename, int $flags = 0, ?array $readers = null, array $mergeArray = []): Spreadsheet
|
||||
{
|
||||
$reader = self::createReaderForFile($filename, $readers);
|
||||
$reader = self::createReaderForFile($filename, $readers, $mergeArray);
|
||||
|
||||
return $reader->load($filename, $flags);
|
||||
}
|
||||
@@ -118,10 +151,11 @@ abstract class IOFactory
|
||||
* Identify file type using automatic IReader resolution.
|
||||
*
|
||||
* @param string[] $readers
|
||||
* @param array<string, class-string<IReader>> $mergeArray
|
||||
*/
|
||||
public static function identify(string $filename, ?array $readers = null, bool $fullClassName = false): string
|
||||
public static function identify(string $filename, ?array $readers = null, bool $fullClassName = false, array $mergeArray = []): string
|
||||
{
|
||||
$reader = self::createReaderForFile($filename, $readers);
|
||||
$reader = self::createReaderForFile($filename, $readers, $mergeArray);
|
||||
$className = $reader::class;
|
||||
if ($fullClassName) {
|
||||
return $className;
|
||||
@@ -139,12 +173,16 @@ abstract class IOFactory
|
||||
* list of Readers so it will only try the subset that you specify here.
|
||||
* Values in this list can be any of the constant values defined in the set
|
||||
* IOFactory::READER_*.
|
||||
* @param array<string, class-string<IReader>> $mergeArray supplied readers will be merged with
|
||||
* default readers, allowing specification of a partial list.
|
||||
*
|
||||
* @throws Reader\Exception
|
||||
*/
|
||||
public static function createReaderForFile(string $filename, ?array $readers = null): IReader
|
||||
public static function createReaderForFile(string $filename, ?array $readers = null, array $mergeArray = []): IReader
|
||||
{
|
||||
File::assertFile($filename);
|
||||
|
||||
$testReaders = self::$readers;
|
||||
$testReaders = array_merge(self::$readers, $mergeArray);
|
||||
if ($readers !== null) {
|
||||
$readers = array_map('strtoupper', $readers);
|
||||
$testReaders = array_filter(
|
||||
@@ -157,7 +195,7 @@ abstract class IOFactory
|
||||
// First, lucky guess by inspecting file extension
|
||||
$guessedReader = self::getReaderTypeFromExtension($filename);
|
||||
if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) {
|
||||
$reader = self::createReader($guessedReader);
|
||||
$reader = self::createReader($guessedReader, $testReaders);
|
||||
|
||||
// Let's see if we are lucky
|
||||
if ($reader->canRead($filename)) {
|
||||
@@ -170,7 +208,7 @@ abstract class IOFactory
|
||||
foreach ($testReaders as $readerType => $class) {
|
||||
// Ignore our original guess, we know that won't work
|
||||
if ($readerType !== $guessedReader) {
|
||||
$reader = self::createReader($readerType);
|
||||
$reader = self::createReader($readerType, $testReaders);
|
||||
if ($reader->canRead($filename)) {
|
||||
return $reader;
|
||||
}
|
||||
@@ -227,8 +265,7 @@ abstract class IOFactory
|
||||
*/
|
||||
public static function registerWriter(string $writerType, string $writerClass): void
|
||||
{
|
||||
// We want phpstan to validate caller, but still need this test
|
||||
if (!is_a($writerClass, IWriter::class, true)) { //* @phpstan-ignore-line
|
||||
if (!is_a($writerClass, IWriter::class, true)) {
|
||||
throw new Writer\Exception('Registered writers must implement ' . IWriter::class);
|
||||
}
|
||||
|
||||
@@ -242,11 +279,22 @@ abstract class IOFactory
|
||||
*/
|
||||
public static function registerReader(string $readerType, string $readerClass): void
|
||||
{
|
||||
// We want phpstan to validate caller, but still need this test
|
||||
if (!is_a($readerClass, IReader::class, true)) { //* @phpstan-ignore-line
|
||||
if (!is_a($readerClass, IReader::class, true)) {
|
||||
throw new Reader\Exception('Registered readers must implement ' . IReader::class);
|
||||
}
|
||||
|
||||
self::$readers[$readerType] = $readerClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, class-string<IReader>>
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
public static function getReaders(): array
|
||||
{
|
||||
return self::$readers;
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user