Merge branch 'master' into Further-Test-Refactoring
This commit is contained in:
commit
ee5134a954
210
CHANGELOG.md
210
CHANGELOG.md
|
@ -9,33 +9,55 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Added
|
||||
|
||||
- Implementation of IFNA() Logical Function
|
||||
- When <br> appears in a table cell, set the cell to wrap [Issue #1071](https://github.com/PHPOffice/PhpSpreadsheet/issues/1071) and [PR #1070](https://github.com/PHPOffice/PhpSpreadsheet/pull/1070)
|
||||
- Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF - [Issue #1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056)
|
||||
- HLookup needs an ordered list even if range_lookup is set to false [Issue #1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [PR #1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076)
|
||||
- Implementation of IFNA() logical function
|
||||
|
||||
### Fixed
|
||||
|
||||
- ...
|
||||
|
||||
## [1.9.0] - 2019-08-17
|
||||
|
||||
### Changed
|
||||
|
||||
- Drop support for PHP 5.6 and 7.0, according to https://phpspreadsheet.readthedocs.io/en/latest/#php-version-support
|
||||
|
||||
### Added
|
||||
|
||||
- When <br> appears in a table cell, set the cell to wrap [#1071](https://github.com/PHPOffice/PhpSpreadsheet/issues/1071) and [#1070](https://github.com/PHPOffice/PhpSpreadsheet/pull/1070)
|
||||
- Add MAXIFS, MINIFS, COUNTIFS and Remove MINIF, MAXIF [#1056](https://github.com/PHPOffice/PhpSpreadsheet/issues/1056)
|
||||
- HLookup needs an ordered list even if range_lookup is set to false [#1055](https://github.com/PHPOffice/PhpSpreadsheet/issues/1055) and [#1076](https://github.com/PHPOffice/PhpSpreadsheet/pull/1076)
|
||||
- Improve performance of IF function calls via ranch pruning to avoid resolution of every branches [#844](https://github.com/PHPOffice/PhpSpreadsheet/pull/844)
|
||||
- MATCH function supports `*?~` Excel functionality, when match_type=0 [#1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116)
|
||||
- Allow HTML Reader to accept HTML as a string [#1136](https://github.com/PHPOffice/PhpSpreadsheet/pull/1136)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix to AVERAGEIF() function when called with a third argument
|
||||
- Eliminate duplicate fill none style entries [Issue #1066](https://github.com/PHPOffice/PhpSpreadsheet/issues/1066)
|
||||
- Fix number format masks containing literal (non-decimal point) dots [Issue #1079](https://github.com/PHPOffice/PhpSpreadsheet/issues/1079)
|
||||
- Fix number format masks containing named colours that were being misinterpreted as date formats; and add support for masks that fully replace the value with a full text string [Issue #1009](https://github.com/PHPOffice/PhpSpreadsheet/issues/1009)
|
||||
- Stricter-typed comparison testing in COUNTIF() and COUNTIFS() evaluation [Issue #1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046)
|
||||
- COUPNUM should not return zero when settlement is in the last period - [Issue #1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [PR #1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021)
|
||||
- Eliminate duplicate fill none style entries [#1066](https://github.com/PHPOffice/PhpSpreadsheet/issues/1066)
|
||||
- Fix number format masks containing literal (non-decimal point) dots [#1079](https://github.com/PHPOffice/PhpSpreadsheet/issues/1079)
|
||||
- Fix number format masks containing named colours that were being misinterpreted as date formats; and add support for masks that fully replace the value with a full text string [#1009](https://github.com/PHPOffice/PhpSpreadsheet/issues/1009)
|
||||
- Stricter-typed comparison testing in COUNTIF() and COUNTIFS() evaluation [#1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046)
|
||||
- COUPNUM should not return zero when settlement is in the last period [#1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [#1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021)
|
||||
- Fix handling of named ranges referencing sheets with spaces or "!" in their title
|
||||
- Cover `getSheetByName()` with tests for name with quote and spaces [#739](https://github.com/PHPOffice/PhpSpreadsheet/issues/739)
|
||||
- Best effort to support invalid colspan values in HTML reader - [#878](https://github.com/PHPOffice/PhpSpreadsheet/pull/878)
|
||||
- Fixes incorrect rows deletion [#868](https://github.com/PHPOffice/PhpSpreadsheet/issues/868)
|
||||
- MATCH function fix (value search by type, stop search when match_type=-1 and unordered element encountered) [#1116](https://github.com/PHPOffice/PhpSpreadsheet/issues/1116)
|
||||
- Fix `getCalculatedValue()` error with more than two INDIRECT [#1115](https://github.com/PHPOffice/PhpSpreadsheet/pull/1115)
|
||||
- Writer\Html did not hide columns [#985](https://github.com/PHPOffice/PhpSpreadsheet/pull/985)
|
||||
|
||||
## [1.8.2] - 2019-07-08
|
||||
|
||||
### Fixed
|
||||
|
||||
- Uncaught error when opening ods file and properties aren't defined - [Issue #1047](https://github.com/PHPOffice/PhpSpreadsheet/issues/1047)
|
||||
- Xlsx Reader Cell datavalidations bug - [PR #1052](https://github.com/PHPOffice/PhpSpreadsheet/pull/1052)
|
||||
- Uncaught error when opening ods file and properties aren't defined [#1047](https://github.com/PHPOffice/PhpSpreadsheet/issues/1047)
|
||||
- Xlsx Reader Cell datavalidations bug [#1052](https://github.com/PHPOffice/PhpSpreadsheet/pull/1052)
|
||||
|
||||
## [1.8.1] - 2019-07-02
|
||||
|
||||
### Fixed
|
||||
|
||||
- Allow nullable theme for Xlsx Style Reader class - [Issue #1043](https://github.com/PHPOffice/PhpSpreadsheet/issues/1043)
|
||||
- Allow nullable theme for Xlsx Style Reader class [#1043](https://github.com/PHPOffice/PhpSpreadsheet/issues/1043)
|
||||
|
||||
## [1.8.0] - 2019-07-01
|
||||
|
||||
|
@ -51,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Added
|
||||
|
||||
- Added support for the SWITCH function - [Issue #963](https://github.com/PHPOffice/PhpSpreadsheet/issues/963) and [PR #983](https://github.com/PHPOffice/PhpSpreadsheet/pull/983)
|
||||
- Added support for the SWITCH function [#963](https://github.com/PHPOffice/PhpSpreadsheet/issues/963) and [#983](https://github.com/PHPOffice/PhpSpreadsheet/pull/983)
|
||||
- Add accounting number format style [#974](https://github.com/PHPOffice/PhpSpreadsheet/pull/974)
|
||||
|
||||
### Fixed
|
||||
|
@ -79,24 +101,24 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
### Added
|
||||
|
||||
- Refactored Matrix Functions to use external Matrix library
|
||||
- Possibility to specify custom colors of values for pie and donut charts - [#768](https://github.com/PHPOffice/PhpSpreadsheet/pull/768)
|
||||
- Possibility to specify custom colors of values for pie and donut charts [#768](https://github.com/PHPOffice/PhpSpreadsheet/pull/768)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Improve XLSX parsing speed if no readFilter is applied - [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772)
|
||||
- Fix column names if read filter calls in XLSX reader skip columns - [#777](https://github.com/PHPOffice/PhpSpreadsheet/pull/777)
|
||||
- XLSX reader can now ignore blank cells, using the setReadEmptyCells(false) method. - [#810](https://github.com/PHPOffice/PhpSpreadsheet/issues/810)
|
||||
- Fix LOOKUP function which was breaking on edge cases - [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796)
|
||||
- Fix VLOOKUP with exact matches - [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809)
|
||||
- Support COUNTIFS multiple arguments - [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830)
|
||||
- Change `libxml_disable_entity_loader()` as shortly as possible - [#819](https://github.com/PHPOffice/PhpSpreadsheet/pull/819)
|
||||
- Improved memory usage and performance when loading large spreadsheets - [#822](https://github.com/PHPOffice/PhpSpreadsheet/pull/822)
|
||||
- Improved performance when loading large spreadsheets - [#825](https://github.com/PHPOffice/PhpSpreadsheet/pull/825)
|
||||
- Improved performance when loading large spreadsheets - [#824](https://github.com/PHPOffice/PhpSpreadsheet/pull/824)
|
||||
- Fix color from CSS when reading from HTML - [#831](https://github.com/PHPOffice/PhpSpreadsheet/pull/831)
|
||||
- Fix infinite loop when reading invalid ODS files - [#832](https://github.com/PHPOffice/PhpSpreadsheet/pull/832)
|
||||
- Fix time format for duration is incorrect - [#666](https://github.com/PHPOffice/PhpSpreadsheet/pull/666)
|
||||
- Fix iconv unsupported `//IGNORE//TRANSLIT` on IBM i - [#791](https://github.com/PHPOffice/PhpSpreadsheet/issues/791)
|
||||
- Improve XLSX parsing speed if no readFilter is applied [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772)
|
||||
- Fix column names if read filter calls in XLSX reader skip columns [#777](https://github.com/PHPOffice/PhpSpreadsheet/pull/777)
|
||||
- XLSX reader can now ignore blank cells, using the setReadEmptyCells(false) method. [#810](https://github.com/PHPOffice/PhpSpreadsheet/issues/810)
|
||||
- Fix LOOKUP function which was breaking on edge cases [#796](https://github.com/PHPOffice/PhpSpreadsheet/issues/796)
|
||||
- Fix VLOOKUP with exact matches [#809](https://github.com/PHPOffice/PhpSpreadsheet/pull/809)
|
||||
- Support COUNTIFS multiple arguments [#830](https://github.com/PHPOffice/PhpSpreadsheet/pull/830)
|
||||
- Change `libxml_disable_entity_loader()` as shortly as possible [#819](https://github.com/PHPOffice/PhpSpreadsheet/pull/819)
|
||||
- Improved memory usage and performance when loading large spreadsheets [#822](https://github.com/PHPOffice/PhpSpreadsheet/pull/822)
|
||||
- Improved performance when loading large spreadsheets [#825](https://github.com/PHPOffice/PhpSpreadsheet/pull/825)
|
||||
- Improved performance when loading large spreadsheets [#824](https://github.com/PHPOffice/PhpSpreadsheet/pull/824)
|
||||
- Fix color from CSS when reading from HTML [#831](https://github.com/PHPOffice/PhpSpreadsheet/pull/831)
|
||||
- Fix infinite loop when reading invalid ODS files [#832](https://github.com/PHPOffice/PhpSpreadsheet/pull/832)
|
||||
- Fix time format for duration is incorrect [#666](https://github.com/PHPOffice/PhpSpreadsheet/pull/666)
|
||||
- Fix iconv unsupported `//IGNORE//TRANSLIT` on IBM i [#791](https://github.com/PHPOffice/PhpSpreadsheet/issues/791)
|
||||
|
||||
### Changed
|
||||
|
||||
|
@ -106,59 +128,59 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Security
|
||||
|
||||
- Improvements to the design of the XML Security Scanner - [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771)
|
||||
- Improvements to the design of the XML Security Scanner [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771)
|
||||
|
||||
## [1.5.1] - 2018-11-20
|
||||
|
||||
### Security
|
||||
|
||||
- Fix and improve XXE security scanning for XML-based and HTML Readers - [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771)
|
||||
- Fix and improve XXE security scanning for XML-based and HTML Readers [#771](https://github.com/PHPOffice/PhpSpreadsheet/issues/771)
|
||||
|
||||
### Added
|
||||
|
||||
- Support page margin in mPDF - [#750](https://github.com/PHPOffice/PhpSpreadsheet/issues/750)
|
||||
- Support page margin in mPDF [#750](https://github.com/PHPOffice/PhpSpreadsheet/issues/750)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Support numeric condition in SUMIF, SUMIFS, AVERAGEIF, COUNTIF, MAXIF and MINIF - [#683](https://github.com/PHPOffice/PhpSpreadsheet/issues/683)
|
||||
- SUMIFS containing multiple conditions - [#704](https://github.com/PHPOffice/PhpSpreadsheet/issues/704)
|
||||
- Csv reader avoid notice when the file is empty - [#743](https://github.com/PHPOffice/PhpSpreadsheet/pull/743)
|
||||
- Fix print area parser for XLSX reader - [#734](https://github.com/PHPOffice/PhpSpreadsheet/pull/734)
|
||||
- Support overriding `DefaultValueBinder::dataTypeForValue()` without overriding `DefaultValueBinder::bindValue()` - [#735](https://github.com/PHPOffice/PhpSpreadsheet/pull/735)
|
||||
- Mpdf export can exceed pcre.backtrack_limit - [#637](https://github.com/PHPOffice/PhpSpreadsheet/issues/637)
|
||||
- Fix index overflow on data values array - [#748](https://github.com/PHPOffice/PhpSpreadsheet/pull/748)
|
||||
- Support numeric condition in SUMIF, SUMIFS, AVERAGEIF, COUNTIF, MAXIF and MINIF [#683](https://github.com/PHPOffice/PhpSpreadsheet/issues/683)
|
||||
- SUMIFS containing multiple conditions [#704](https://github.com/PHPOffice/PhpSpreadsheet/issues/704)
|
||||
- Csv reader avoid notice when the file is empty [#743](https://github.com/PHPOffice/PhpSpreadsheet/pull/743)
|
||||
- Fix print area parser for XLSX reader [#734](https://github.com/PHPOffice/PhpSpreadsheet/pull/734)
|
||||
- Support overriding `DefaultValueBinder::dataTypeForValue()` without overriding `DefaultValueBinder::bindValue()` [#735](https://github.com/PHPOffice/PhpSpreadsheet/pull/735)
|
||||
- Mpdf export can exceed pcre.backtrack_limit [#637](https://github.com/PHPOffice/PhpSpreadsheet/issues/637)
|
||||
- Fix index overflow on data values array [#748](https://github.com/PHPOffice/PhpSpreadsheet/pull/748)
|
||||
|
||||
## [1.5.0] - 2018-10-21
|
||||
|
||||
### Added
|
||||
|
||||
- PHP 7.3 support
|
||||
- Add the DAYS() function - [#594](https://github.com/PHPOffice/PhpSpreadsheet/pull/594)
|
||||
- Add the DAYS() function [#594](https://github.com/PHPOffice/PhpSpreadsheet/pull/594)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Sheet title can contain exclamation mark - [#325](https://github.com/PHPOffice/PhpSpreadsheet/issues/325)
|
||||
- Xls file cause the exception during open by Xls reader - [#402](https://github.com/PHPOffice/PhpSpreadsheet/issues/402)
|
||||
- Skip non numeric value in SUMIF - [#618](https://github.com/PHPOffice/PhpSpreadsheet/pull/618)
|
||||
- OFFSET should allow omitted height and width - [#561](https://github.com/PHPOffice/PhpSpreadsheet/issues/561)
|
||||
- Correctly determine delimiter when CSV contains line breaks inside enclosures - [#716](https://github.com/PHPOffice/PhpSpreadsheet/issues/716)
|
||||
- Sheet title can contain exclamation mark [#325](https://github.com/PHPOffice/PhpSpreadsheet/issues/325)
|
||||
- Xls file cause the exception during open by Xls reader [#402](https://github.com/PHPOffice/PhpSpreadsheet/issues/402)
|
||||
- Skip non numeric value in SUMIF [#618](https://github.com/PHPOffice/PhpSpreadsheet/pull/618)
|
||||
- OFFSET should allow omitted height and width [#561](https://github.com/PHPOffice/PhpSpreadsheet/issues/561)
|
||||
- Correctly determine delimiter when CSV contains line breaks inside enclosures [#716](https://github.com/PHPOffice/PhpSpreadsheet/issues/716)
|
||||
|
||||
## [1.4.1] - 2018-09-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- Remove locale from formatting string - [#644](https://github.com/PHPOffice/PhpSpreadsheet/pull/644)
|
||||
- Allow iterators to go out of bounds with prev - [#587](https://github.com/PHPOffice/PhpSpreadsheet/issues/587)
|
||||
- Fix warning when reading xlsx without styles - [#631](https://github.com/PHPOffice/PhpSpreadsheet/pull/631)
|
||||
- Fix broken sample links on windows due to $baseDir having backslash - [#653](https://github.com/PHPOffice/PhpSpreadsheet/pull/653)
|
||||
- Remove locale from formatting string [#644](https://github.com/PHPOffice/PhpSpreadsheet/pull/644)
|
||||
- Allow iterators to go out of bounds with prev [#587](https://github.com/PHPOffice/PhpSpreadsheet/issues/587)
|
||||
- Fix warning when reading xlsx without styles [#631](https://github.com/PHPOffice/PhpSpreadsheet/pull/631)
|
||||
- Fix broken sample links on windows due to $baseDir having backslash [#653](https://github.com/PHPOffice/PhpSpreadsheet/pull/653)
|
||||
|
||||
## [1.4.0] - 2018-08-06
|
||||
|
||||
### Added
|
||||
|
||||
- Add excel function EXACT(value1, value2) support - [#595](https://github.com/PHPOffice/PhpSpreadsheet/pull/595)
|
||||
- Support workbook view attributes for Xlsx format - [#523](https://github.com/PHPOffice/PhpSpreadsheet/issues/523)
|
||||
- Read and write hyperlink for drawing image - [#490](https://github.com/PHPOffice/PhpSpreadsheet/pull/490)
|
||||
- Add excel function EXACT(value1, value2) support [#595](https://github.com/PHPOffice/PhpSpreadsheet/pull/595)
|
||||
- Support workbook view attributes for Xlsx format [#523](https://github.com/PHPOffice/PhpSpreadsheet/issues/523)
|
||||
- Read and write hyperlink for drawing image [#490](https://github.com/PHPOffice/PhpSpreadsheet/pull/490)
|
||||
- Added calculation engine support for the new bitwise functions that were added in MS Excel 2013
|
||||
- BITAND() Returns a Bitwise 'And' of two numbers
|
||||
- BITOR() Returns a Bitwise 'Or' of two number
|
||||
|
@ -207,10 +229,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
### Fixed
|
||||
|
||||
- Fix ISFORMULA() function to work with a cell reference to another worksheet
|
||||
- Xlsx reader crashed when reading a file with workbook protection - [#553](https://github.com/PHPOffice/PhpSpreadsheet/pull/553)
|
||||
- Cell formats with escaped spaces were causing incorrect date formatting - [#557](https://github.com/PHPOffice/PhpSpreadsheet/issues/557)
|
||||
- Could not open CSV file containing HTML fragment - [#564](https://github.com/PHPOffice/PhpSpreadsheet/issues/564)
|
||||
- Exclude the vendor folder in migration - [#481](https://github.com/PHPOffice/PhpSpreadsheet/issues/481)
|
||||
- Xlsx reader crashed when reading a file with workbook protection [#553](https://github.com/PHPOffice/PhpSpreadsheet/pull/553)
|
||||
- Cell formats with escaped spaces were causing incorrect date formatting [#557](https://github.com/PHPOffice/PhpSpreadsheet/issues/557)
|
||||
- Could not open CSV file containing HTML fragment [#564](https://github.com/PHPOffice/PhpSpreadsheet/issues/564)
|
||||
- Exclude the vendor folder in migration [#481](https://github.com/PHPOffice/PhpSpreadsheet/issues/481)
|
||||
- Chained operations on cell ranges involving borders operated on last cell only [#428](https://github.com/PHPOffice/PhpSpreadsheet/issues/428)
|
||||
- Avoid memory exhaustion when cloning worksheet with a drawing [#437](https://github.com/PHPOffice/PhpSpreadsheet/issues/437)
|
||||
- Migration tool keep variables containing $PHPExcel untouched [#598](https://github.com/PHPOffice/PhpSpreadsheet/issues/598)
|
||||
|
@ -220,83 +242,83 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
### Fixed
|
||||
|
||||
- Ranges across Z and AA columns incorrectly threw an exception - [#545](https://github.com/PHPOffice/PhpSpreadsheet/issues/545)
|
||||
- Ranges across Z and AA columns incorrectly threw an exception [#545](https://github.com/PHPOffice/PhpSpreadsheet/issues/545)
|
||||
|
||||
## [1.3.0] - 2018-06-10
|
||||
|
||||
### Added
|
||||
|
||||
- Support to read Xlsm templates with form elements, macros, printer settings, protected elements and back compatibility drawing, and save result without losing important elements of document - [#435](https://github.com/PHPOffice/PhpSpreadsheet/issues/435)
|
||||
- Expose sheet title maximum length as `Worksheet::SHEET_TITLE_MAXIMUM_LENGTH` - [#482](https://github.com/PHPOffice/PhpSpreadsheet/issues/482)
|
||||
- Allow escape character to be set in CSV reader – [#492](https://github.com/PHPOffice/PhpSpreadsheet/issues/492)
|
||||
- Support to read Xlsm templates with form elements, macros, printer settings, protected elements and back compatibility drawing, and save result without losing important elements of document [#435](https://github.com/PHPOffice/PhpSpreadsheet/issues/435)
|
||||
- Expose sheet title maximum length as `Worksheet::SHEET_TITLE_MAXIMUM_LENGTH` [#482](https://github.com/PHPOffice/PhpSpreadsheet/issues/482)
|
||||
- Allow escape character to be set in CSV reader [#492](https://github.com/PHPOffice/PhpSpreadsheet/issues/492)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Subtotal 9 in a group that has other subtotals 9 exclude the totals of the other subtotals in the range - [#332](https://github.com/PHPOffice/PhpSpreadsheet/issues/332)
|
||||
- `Helper\Html` support UTF-8 HTML input - [#444](https://github.com/PHPOffice/PhpSpreadsheet/issues/444)
|
||||
- Xlsx loaded an extra empty comment for each real comment - [#375](https://github.com/PHPOffice/PhpSpreadsheet/issues/375)
|
||||
- Xlsx reader do not read rows and columns filtered out in readFilter at all - [#370](https://github.com/PHPOffice/PhpSpreadsheet/issues/370)
|
||||
- Make newer Excel versions properly recalculate formulas on document open - [#456](https://github.com/PHPOffice/PhpSpreadsheet/issues/456)
|
||||
- `Coordinate::extractAllCellReferencesInRange()` throws an exception for an invalid range – [#519](https://github.com/PHPOffice/PhpSpreadsheet/issues/519)
|
||||
- Fixed parsing of conditionals in COUNTIF functions - [#526](https://github.com/PHPOffice/PhpSpreadsheet/issues/526)
|
||||
- Corruption errors for saved Xlsx docs with frozen panes - [#532](https://github.com/PHPOffice/PhpSpreadsheet/issues/532)
|
||||
- Subtotal 9 in a group that has other subtotals 9 exclude the totals of the other subtotals in the range [#332](https://github.com/PHPOffice/PhpSpreadsheet/issues/332)
|
||||
- `Helper\Html` support UTF-8 HTML input [#444](https://github.com/PHPOffice/PhpSpreadsheet/issues/444)
|
||||
- Xlsx loaded an extra empty comment for each real comment [#375](https://github.com/PHPOffice/PhpSpreadsheet/issues/375)
|
||||
- Xlsx reader do not read rows and columns filtered out in readFilter at all [#370](https://github.com/PHPOffice/PhpSpreadsheet/issues/370)
|
||||
- Make newer Excel versions properly recalculate formulas on document open [#456](https://github.com/PHPOffice/PhpSpreadsheet/issues/456)
|
||||
- `Coordinate::extractAllCellReferencesInRange()` throws an exception for an invalid range [#519](https://github.com/PHPOffice/PhpSpreadsheet/issues/519)
|
||||
- Fixed parsing of conditionals in COUNTIF functions [#526](https://github.com/PHPOffice/PhpSpreadsheet/issues/526)
|
||||
- Corruption errors for saved Xlsx docs with frozen panes [#532](https://github.com/PHPOffice/PhpSpreadsheet/issues/532)
|
||||
|
||||
## [1.2.1] - 2018-04-10
|
||||
|
||||
### Fixed
|
||||
|
||||
- Plain text and richtext mixed in same cell can be read - [#442](https://github.com/PHPOffice/PhpSpreadsheet/issues/442)
|
||||
- Plain text and richtext mixed in same cell can be read [#442](https://github.com/PHPOffice/PhpSpreadsheet/issues/442)
|
||||
|
||||
## [1.2.0] - 2018-03-04
|
||||
|
||||
### Added
|
||||
|
||||
- HTML writer creates a generator meta tag - [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312)
|
||||
- Support invalid zoom value in XLSX format - [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350)
|
||||
- Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` - [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390)
|
||||
- HTML writer creates a generator meta tag [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312)
|
||||
- Support invalid zoom value in XLSX format [#350](https://github.com/PHPOffice/PhpSpreadsheet/pull/350)
|
||||
- Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Avoid potentially unsupported PSR-16 cache keys - [#354](https://github.com/PHPOffice/PhpSpreadsheet/issues/354)
|
||||
- Check for MIME type to know if CSV reader can read a file - [#167](https://github.com/PHPOffice/PhpSpreadsheet/issues/167)
|
||||
- Use proper € symbol for currency format - [#379](https://github.com/PHPOffice/PhpSpreadsheet/pull/379)
|
||||
- Read printing area correctly when skipping some sheets - [#371](https://github.com/PHPOffice/PhpSpreadsheet/issues/371)
|
||||
- Avoid incorrectly overwriting calculated value type - [#394](https://github.com/PHPOffice/PhpSpreadsheet/issues/394)
|
||||
- Select correct cell when calling freezePane - [#389](https://github.com/PHPOffice/PhpSpreadsheet/issues/389)
|
||||
- `setStrikethrough()` did not set the font - [#403](https://github.com/PHPOffice/PhpSpreadsheet/issues/403)
|
||||
- Avoid potentially unsupported PSR-16 cache keys [#354](https://github.com/PHPOffice/PhpSpreadsheet/issues/354)
|
||||
- Check for MIME type to know if CSV reader can read a file [#167](https://github.com/PHPOffice/PhpSpreadsheet/issues/167)
|
||||
- Use proper € symbol for currency format [#379](https://github.com/PHPOffice/PhpSpreadsheet/pull/379)
|
||||
- Read printing area correctly when skipping some sheets [#371](https://github.com/PHPOffice/PhpSpreadsheet/issues/371)
|
||||
- Avoid incorrectly overwriting calculated value type [#394](https://github.com/PHPOffice/PhpSpreadsheet/issues/394)
|
||||
- Select correct cell when calling freezePane [#389](https://github.com/PHPOffice/PhpSpreadsheet/issues/389)
|
||||
- `setStrikethrough()` did not set the font [#403](https://github.com/PHPOffice/PhpSpreadsheet/issues/403)
|
||||
|
||||
## [1.1.0] - 2018-01-28
|
||||
|
||||
### Added
|
||||
|
||||
- Support for PHP 7.2
|
||||
- Support cell comments in HTML writer and reader - [#308](https://github.com/PHPOffice/PhpSpreadsheet/issues/308)
|
||||
- Option to stop at a conditional styling, if it matches (only XLSX format) - [#292](https://github.com/PHPOffice/PhpSpreadsheet/pull/292)
|
||||
- Support for line width for data series when rendering Xlsx - [#329](https://github.com/PHPOffice/PhpSpreadsheet/pull/329)
|
||||
- Support cell comments in HTML writer and reader [#308](https://github.com/PHPOffice/PhpSpreadsheet/issues/308)
|
||||
- Option to stop at a conditional styling, if it matches (only XLSX format) [#292](https://github.com/PHPOffice/PhpSpreadsheet/pull/292)
|
||||
- Support for line width for data series when rendering Xlsx [#329](https://github.com/PHPOffice/PhpSpreadsheet/pull/329)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Better auto-detection of CSV separators - [#305](https://github.com/PHPOffice/PhpSpreadsheet/issues/305)
|
||||
- Support for shape style ending with `;` - [#304](https://github.com/PHPOffice/PhpSpreadsheet/issues/304)
|
||||
- Freeze Panes takes wrong coordinates for XLSX - [#322](https://github.com/PHPOffice/PhpSpreadsheet/issues/322)
|
||||
- `COLUMNS` and `ROWS` functions crashed in some cases - [#336](https://github.com/PHPOffice/PhpSpreadsheet/issues/336)
|
||||
- Support XML file without styles - [#331](https://github.com/PHPOffice/PhpSpreadsheet/pull/331)
|
||||
- Better auto-detection of CSV separators [#305](https://github.com/PHPOffice/PhpSpreadsheet/issues/305)
|
||||
- Support for shape style ending with `;` [#304](https://github.com/PHPOffice/PhpSpreadsheet/issues/304)
|
||||
- Freeze Panes takes wrong coordinates for XLSX [#322](https://github.com/PHPOffice/PhpSpreadsheet/issues/322)
|
||||
- `COLUMNS` and `ROWS` functions crashed in some cases [#336](https://github.com/PHPOffice/PhpSpreadsheet/issues/336)
|
||||
- Support XML file without styles [#331](https://github.com/PHPOffice/PhpSpreadsheet/pull/331)
|
||||
- Cell coordinates which are already a range cause an exception [#319](https://github.com/PHPOffice/PhpSpreadsheet/issues/319)
|
||||
|
||||
## [1.0.0] - 2017-12-25
|
||||
|
||||
### Added
|
||||
|
||||
- Support to write merged cells in ODS format - [#287](https://github.com/PHPOffice/PhpSpreadsheet/issues/287)
|
||||
- Able to set the `topLeftCell` in freeze panes - [#261](https://github.com/PHPOffice/PhpSpreadsheet/pull/261)
|
||||
- Support to write merged cells in ODS format [#287](https://github.com/PHPOffice/PhpSpreadsheet/issues/287)
|
||||
- Able to set the `topLeftCell` in freeze panes [#261](https://github.com/PHPOffice/PhpSpreadsheet/pull/261)
|
||||
- Support `DateTimeImmutable` as cell value
|
||||
- Support migration of prefixed classes
|
||||
|
||||
### Fixed
|
||||
|
||||
- Can read very small HTML files - [#194](https://github.com/PHPOffice/PhpSpreadsheet/issues/194)
|
||||
- Written DataValidation was corrupted - [#290](https://github.com/PHPOffice/PhpSpreadsheet/issues/290)
|
||||
- Date format compatible with both LibreOffice and Excel - [#298](https://github.com/PHPOffice/PhpSpreadsheet/issues/298)
|
||||
- Can read very small HTML files [#194](https://github.com/PHPOffice/PhpSpreadsheet/issues/194)
|
||||
- Written DataValidation was corrupted [#290](https://github.com/PHPOffice/PhpSpreadsheet/issues/290)
|
||||
- Date format compatible with both LibreOffice and Excel [#298](https://github.com/PHPOffice/PhpSpreadsheet/issues/298)
|
||||
|
||||
### BREAKING CHANGE
|
||||
|
||||
|
@ -315,13 +337,13 @@ and this project adheres to [Semantic Versioning](https://semver.org).
|
|||
|
||||
- Merge data-validations to reduce written worksheet size - @billblume [#131](https://github.com/PHPOffice/PhpSpreadSheet/issues/131)
|
||||
- Throws exception if a XML file is invalid - @GreatHumorist [#222](https://github.com/PHPOffice/PhpSpreadsheet/pull/222)
|
||||
- Upgrade to mPDF 7.0+ - [#144](https://github.com/PHPOffice/PhpSpreadsheet/issues/144)
|
||||
- Upgrade to mPDF 7.0+ [#144](https://github.com/PHPOffice/PhpSpreadsheet/issues/144)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Control characters in cell values are automatically escaped - [#212](https://github.com/PHPOffice/PhpSpreadsheet/issues/212)
|
||||
- Control characters in cell values are automatically escaped [#212](https://github.com/PHPOffice/PhpSpreadsheet/issues/212)
|
||||
- Prevent color changing when copy/pasting xls files written by PhpSpreadsheet to another file - @al-lala [#218](https://github.com/PHPOffice/PhpSpreadsheet/issues/218)
|
||||
- Add cell reference automatic when there is no cell reference('r' attribute) in Xlsx file. - @GreatHumorist [#225](https://github.com/PHPOffice/PhpSpreadsheet/pull/225) Refer to [issue#201](https://github.com/PHPOffice/PhpSpreadsheet/issues/201)
|
||||
- Add cell reference automatic when there is no cell reference('r' attribute) in Xlsx file. - @GreatHumorist [#225](https://github.com/PHPOffice/PhpSpreadsheet/pull/225) Refer to [#201](https://github.com/PHPOffice/PhpSpreadsheet/issues/201)
|
||||
- `Reader\Xlsx::getFromZipArchive()` function return false if the zip entry could not be located. - @anton-harvey [#268](https://github.com/PHPOffice/PhpSpreadsheet/pull/268)
|
||||
|
||||
### BREAKING CHANGE
|
||||
|
|
|
@ -173,10 +173,9 @@ code:
|
|||
$writer->setOffice2003Compatibility(true);
|
||||
$writer->save("05featuredemo.xlsx");
|
||||
|
||||
**Office2003 compatibility should only be used when needed** Office2003
|
||||
compatibility option should only be used when needed. This option
|
||||
disables several Office2007 file format options, resulting in a
|
||||
lower-featured Office2007 spreadsheet when this option is used.
|
||||
**Office2003 compatibility option should only be used when needed** because
|
||||
it disables several Office2007 file format options, resulting in a
|
||||
lower-featured Office2007 spreadsheet.
|
||||
|
||||
## Excel 5 (BIFF) file format
|
||||
|
||||
|
@ -875,3 +874,31 @@ $writer->save('write.xls');
|
|||
```
|
||||
|
||||
Notice that it is ok to load an xlsx file and generate an xls file.
|
||||
|
||||
## Generating Excel files from HTML content
|
||||
|
||||
If you are generating an Excel file from pre-rendered HTML content you can do so
|
||||
automatically using the HTML Reader. This is most useful when you are generating
|
||||
Excel files from web application content that would be downloaded/sent to a user.
|
||||
|
||||
For example:
|
||||
|
||||
```php
|
||||
$htmlString = '<table>
|
||||
<tr>
|
||||
<td>Hello World</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hello<br />World</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hello<br>World</td>
|
||||
</tr>
|
||||
</table>';
|
||||
|
||||
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Html();
|
||||
$spreadsheet = $reader->loadFromString($htmlString);
|
||||
|
||||
$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls');
|
||||
$writer->save('write.xls');
|
||||
```
|
||||
|
|
|
@ -66,6 +66,15 @@ class Calculation
|
|||
*/
|
||||
private $calculationCacheEnabled = true;
|
||||
|
||||
/**
|
||||
* Used to generate unique store keys.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $branchStoreKeyCounter = 0;
|
||||
|
||||
private $branchPruningEnabled = true;
|
||||
|
||||
/**
|
||||
* List of operators that can be used within formulae
|
||||
* The true/false value indicates whether it is a binary operator or a unary operator.
|
||||
|
@ -2256,6 +2265,7 @@ class Calculation
|
|||
public function flushInstance()
|
||||
{
|
||||
$this->clearCalculationCache();
|
||||
$this->clearBranchStore();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2399,6 +2409,32 @@ class Calculation
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable calculation cache.
|
||||
*
|
||||
* @param bool $pValue
|
||||
* @param mixed $enabled
|
||||
*/
|
||||
public function setBranchPruningEnabled($enabled)
|
||||
{
|
||||
$this->branchPruningEnabled = $enabled;
|
||||
}
|
||||
|
||||
public function enableBranchPruning()
|
||||
{
|
||||
$this->setBranchPruningEnabled(true);
|
||||
}
|
||||
|
||||
public function disableBranchPruning()
|
||||
{
|
||||
$this->setBranchPruningEnabled(false);
|
||||
}
|
||||
|
||||
public function clearBranchStore()
|
||||
{
|
||||
$this->branchStoreKeyCounter = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently defined locale code.
|
||||
*
|
||||
|
@ -2867,6 +2903,7 @@ class Calculation
|
|||
if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
|
||||
$this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
|
||||
// Return the cached result
|
||||
|
||||
$cellValue = $this->calculationCache[$cellReference];
|
||||
|
||||
return true;
|
||||
|
@ -3326,9 +3363,53 @@ class Calculation
|
|||
// - is a negation or + is a positive operator rather than an operation
|
||||
$expectingOperand = false; // We use this test in syntax-checking the expression to determine whether an operand
|
||||
// should be null in a function call
|
||||
|
||||
// IF branch pruning
|
||||
// currently pending storeKey (last item of the storeKeysStack
|
||||
$pendingStoreKey = null;
|
||||
// stores a list of storeKeys (string[])
|
||||
$pendingStoreKeysStack = [];
|
||||
$expectingConditionMap = []; // ['storeKey' => true, ...]
|
||||
$expectingThenMap = []; // ['storeKey' => true, ...]
|
||||
$expectingElseMap = []; // ['storeKey' => true, ...]
|
||||
$parenthesisDepthMap = []; // ['storeKey' => 4, ...]
|
||||
|
||||
// The guts of the lexical parser
|
||||
// Loop through the formula extracting each operator and operand in turn
|
||||
while (true) {
|
||||
// Branch pruning: we adapt the output item to the context (it will
|
||||
// be used to limit its computation)
|
||||
$currentCondition = null;
|
||||
$currentOnlyIf = null;
|
||||
$currentOnlyIfNot = null;
|
||||
$previousStoreKey = null;
|
||||
$pendingStoreKey = end($pendingStoreKeysStack);
|
||||
|
||||
if ($this->branchPruningEnabled) {
|
||||
// this is a condition ?
|
||||
if (isset($expectingConditionMap[$pendingStoreKey]) && $expectingConditionMap[$pendingStoreKey]) {
|
||||
$currentCondition = $pendingStoreKey;
|
||||
$stackDepth = count($pendingStoreKeysStack);
|
||||
if ($stackDepth > 1) { // nested if
|
||||
$previousStoreKey = $pendingStoreKeysStack[$stackDepth - 2];
|
||||
}
|
||||
}
|
||||
if (isset($expectingThenMap[$pendingStoreKey]) && $expectingThenMap[$pendingStoreKey]) {
|
||||
$currentOnlyIf = $pendingStoreKey;
|
||||
} elseif (isset($previousStoreKey)) {
|
||||
if (isset($expectingThenMap[$previousStoreKey]) && $expectingThenMap[$previousStoreKey]) {
|
||||
$currentOnlyIf = $previousStoreKey;
|
||||
}
|
||||
}
|
||||
if (isset($expectingElseMap[$pendingStoreKey]) && $expectingElseMap[$pendingStoreKey]) {
|
||||
$currentOnlyIfNot = $pendingStoreKey;
|
||||
} elseif (isset($previousStoreKey)) {
|
||||
if (isset($expectingElseMap[$previousStoreKey]) && $expectingElseMap[$previousStoreKey]) {
|
||||
$currentOnlyIfNot = $previousStoreKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$opCharacter = $formula[$index]; // Get the first character of the value at the current index position
|
||||
if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
|
||||
$opCharacter .= $formula[++$index];
|
||||
|
@ -3338,10 +3419,12 @@ class Calculation
|
|||
$isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
|
||||
|
||||
if ($opCharacter == '-' && !$expectingOperator) { // Is it a negation instead of a minus?
|
||||
$stack->push('Unary Operator', '~'); // Put a negation on the stack
|
||||
// Put a negation on the stack
|
||||
$stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
++$index; // and drop the negation symbol
|
||||
} elseif ($opCharacter == '%' && $expectingOperator) {
|
||||
$stack->push('Unary Operator', '%'); // Put a percentage on the stack
|
||||
// Put a percentage on the stack
|
||||
$stack->push('Unary Operator', '%', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
++$index;
|
||||
} elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded?
|
||||
++$index; // Drop the redundant plus symbol
|
||||
|
@ -3354,7 +3437,10 @@ class Calculation
|
|||
@(self::$operatorAssociativity[$opCharacter] ? self::$operatorPrecedence[$opCharacter] < self::$operatorPrecedence[$o2['value']] : self::$operatorPrecedence[$opCharacter] <= self::$operatorPrecedence[$o2['value']])) {
|
||||
$output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output
|
||||
}
|
||||
$stack->push('Binary Operator', $opCharacter); // Finally put our current operator onto the stack
|
||||
|
||||
// Finally put our current operator onto the stack
|
||||
$stack->push('Binary Operator', $opCharacter, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
|
||||
++$index;
|
||||
$expectingOperator = false;
|
||||
} elseif ($opCharacter == ')' && $expectingOperator) { // Are we expecting to close a parenthesis?
|
||||
|
@ -3366,7 +3452,29 @@ class Calculation
|
|||
$output[] = $o2;
|
||||
}
|
||||
$d = $stack->last(2);
|
||||
|
||||
// Branch pruning we decrease the depth whether is it a function
|
||||
// call or a parenthesis
|
||||
if (!empty($pendingStoreKey)) {
|
||||
$parenthesisDepthMap[$pendingStoreKey] -= 1;
|
||||
}
|
||||
|
||||
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $d['value'], $matches)) { // Did this parenthesis just close a function?
|
||||
if (!empty($pendingStoreKey) && $parenthesisDepthMap[$pendingStoreKey] == -1) {
|
||||
// we are closing an IF(
|
||||
if ($d['value'] != 'IF(') {
|
||||
return $this->raiseFormulaError('Parser bug we should be in an "IF("');
|
||||
}
|
||||
if ($expectingConditionMap[$pendingStoreKey]) {
|
||||
return $this->raiseFormulaError('We should not be expecting a condition');
|
||||
}
|
||||
$expectingThenMap[$pendingStoreKey] = false;
|
||||
$expectingElseMap[$pendingStoreKey] = false;
|
||||
$parenthesisDepthMap[$pendingStoreKey] -= 1;
|
||||
array_pop($pendingStoreKeysStack);
|
||||
unset($pendingStoreKey);
|
||||
}
|
||||
|
||||
$functionName = $matches[1]; // Get the function name
|
||||
$d = $stack->pop();
|
||||
$argumentCount = $d['value']; // See how many arguments there were (argument count is the next value stored on the stack)
|
||||
|
@ -3427,6 +3535,20 @@ class Calculation
|
|||
}
|
||||
++$index;
|
||||
} elseif ($opCharacter == ',') { // Is this the separator for function arguments?
|
||||
if (!empty($pendingStoreKey) &&
|
||||
$parenthesisDepthMap[$pendingStoreKey] == 0
|
||||
) {
|
||||
// We must go to the IF next argument
|
||||
if ($expectingConditionMap[$pendingStoreKey]) {
|
||||
$expectingConditionMap[$pendingStoreKey] = false;
|
||||
$expectingThenMap[$pendingStoreKey] = true;
|
||||
} elseif ($expectingThenMap[$pendingStoreKey]) {
|
||||
$expectingThenMap[$pendingStoreKey] = false;
|
||||
$expectingElseMap[$pendingStoreKey] = true;
|
||||
} elseif ($expectingElseMap[$pendingStoreKey]) {
|
||||
return $this->raiseFormulaError('Reaching fourth argument of an IF');
|
||||
}
|
||||
}
|
||||
while (($o2 = $stack->pop()) && $o2['value'] != '(') { // Pop off the stack back to the last (
|
||||
if ($o2 === null) {
|
||||
return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
|
@ -3444,13 +3566,19 @@ class Calculation
|
|||
return $this->raiseFormulaError('Formula Error: Unexpected ,');
|
||||
}
|
||||
$d = $stack->pop();
|
||||
$stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count
|
||||
$stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again
|
||||
$itemStoreKey = $d['storeKey'] ?? null;
|
||||
$itemOnlyIf = $d['onlyIf'] ?? null;
|
||||
$itemOnlyIfNot = $d['onlyIfNot'] ?? null;
|
||||
$stack->push($d['type'], ++$d['value'], $d['reference'], $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // increment the argument count
|
||||
$stack->push('Brace', '(', null, $itemStoreKey, $itemOnlyIf, $itemOnlyIfNot); // put the ( back on, we'll need to pop back to it again
|
||||
$expectingOperator = false;
|
||||
$expectingOperand = true;
|
||||
++$index;
|
||||
} elseif ($opCharacter == '(' && !$expectingOperator) {
|
||||
$stack->push('Brace', '(');
|
||||
if (!empty($pendingStoreKey)) { // Branch pruning: we go deeper
|
||||
$parenthesisDepthMap[$pendingStoreKey] += 1;
|
||||
}
|
||||
$stack->push('Brace', '(', null, $currentCondition, $currentOnlyIf, $currentOnlyIf);
|
||||
++$index;
|
||||
} elseif ($isOperandOrFunction && !$expectingOperator) { // do we now have a function/variable/number?
|
||||
$expectingOperator = true;
|
||||
|
@ -3461,13 +3589,28 @@ class Calculation
|
|||
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
|
||||
$val = preg_replace('/\s/u', '', $val);
|
||||
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function
|
||||
$stack->push('Function', strtoupper($val));
|
||||
$valToUpper = strtoupper($val);
|
||||
// here $matches[1] will contain values like "IF"
|
||||
// and $val "IF("
|
||||
if ($this->branchPruningEnabled && ($valToUpper == 'IF(')) { // we handle a new if
|
||||
$pendingStoreKey = $this->getUnusedBranchStoreKey();
|
||||
$pendingStoreKeysStack[] = $pendingStoreKey;
|
||||
$expectingConditionMap[$pendingStoreKey] = true;
|
||||
$parenthesisDepthMap[$pendingStoreKey] = 0;
|
||||
} else { // this is not a if but we good deeper
|
||||
if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) {
|
||||
$parenthesisDepthMap[$pendingStoreKey] += 1;
|
||||
}
|
||||
}
|
||||
|
||||
$stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
// tests if the function is closed right after opening
|
||||
$ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
|
||||
if ($ax) {
|
||||
$stack->push('Operand Count for Function ' . strtoupper($val) . ')', 0);
|
||||
$stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
$expectingOperator = true;
|
||||
} else {
|
||||
$stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1);
|
||||
$stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
$expectingOperator = false;
|
||||
}
|
||||
$stack->push('Brace', '(');
|
||||
|
@ -3495,7 +3638,9 @@ class Calculation
|
|||
}
|
||||
}
|
||||
|
||||
$output[] = ['type' => 'Cell Reference', 'value' => $val, 'reference' => $val];
|
||||
$outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
|
||||
$output[] = $outputItem;
|
||||
} else { // it's a variable, constant, string, number or boolean
|
||||
// If the last entry on the stack was a : operator, then we may have a row or column range reference
|
||||
$testPrevOp = $stack->last(1);
|
||||
|
@ -3542,7 +3687,7 @@ class Calculation
|
|||
} elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
|
||||
$val = self::$excelConstants[$localeConstant];
|
||||
}
|
||||
$details = ['type' => 'Value', 'value' => $val, 'reference' => null];
|
||||
$details = $stack->getStackItem('Value', $val, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
|
||||
if ($localeConstant) {
|
||||
$details['localeValue'] = $localeConstant;
|
||||
}
|
||||
|
@ -3645,9 +3790,74 @@ class Calculation
|
|||
$pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
|
||||
$stack = new Stack();
|
||||
|
||||
// Stores branches that have been pruned
|
||||
$fakedForBranchPruning = [];
|
||||
// help us to know when pruning ['branchTestId' => true/false]
|
||||
$branchStore = [];
|
||||
|
||||
// Loop through each token in turn
|
||||
foreach ($tokens as $tokenData) {
|
||||
$token = $tokenData['value'];
|
||||
|
||||
// Branch pruning: skip useless resolutions
|
||||
$storeKey = $tokenData['storeKey'] ?? null;
|
||||
if ($this->branchPruningEnabled && isset($tokenData['onlyIf'])) {
|
||||
$onlyIfStoreKey = $tokenData['onlyIf'];
|
||||
$storeValue = $branchStore[$onlyIfStoreKey] ?? null;
|
||||
if (is_array($storeValue)) {
|
||||
$wrappedItem = end($storeValue);
|
||||
$storeValue = end($wrappedItem);
|
||||
}
|
||||
|
||||
if (isset($storeValue) && (($storeValue !== true)
|
||||
|| ($storeValue === 'Pruned branch'))
|
||||
) {
|
||||
// If branching value is not true, we don't need to compute
|
||||
if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) {
|
||||
$stack->push('Value', 'Pruned branch (only if ' . $onlyIfStoreKey . ') ' . $token);
|
||||
$fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey] = true;
|
||||
}
|
||||
|
||||
if (isset($storeKey)) {
|
||||
// We are processing an if condition
|
||||
// We cascade the pruning to the depending branches
|
||||
$branchStore[$storeKey] = 'Pruned branch';
|
||||
$fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
|
||||
$fakedForBranchPruning['onlyIf-' . $storeKey] = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->branchPruningEnabled && isset($tokenData['onlyIfNot'])) {
|
||||
$onlyIfNotStoreKey = $tokenData['onlyIfNot'];
|
||||
$storeValue = $branchStore[$onlyIfNotStoreKey] ?? null;
|
||||
if (is_array($storeValue)) {
|
||||
$wrappedItem = end($storeValue);
|
||||
$storeValue = end($wrappedItem);
|
||||
}
|
||||
if (isset($storeValue) && ($storeValue
|
||||
|| ($storeValue === 'Pruned branch'))
|
||||
) {
|
||||
// If branching value is true, we don't need to compute
|
||||
if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) {
|
||||
$stack->push('Value', 'Pruned branch (only if not ' . $onlyIfNotStoreKey . ') ' . $token);
|
||||
$fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey] = true;
|
||||
}
|
||||
|
||||
if (isset($storeKey)) {
|
||||
// We are processing an if condition
|
||||
// We cascade the pruning to the depending branches
|
||||
$branchStore[$storeKey] = 'Pruned branch';
|
||||
$fakedForBranchPruning['onlyIfNot-' . $storeKey] = true;
|
||||
$fakedForBranchPruning['onlyIf-' . $storeKey] = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if (isset(self::$binaryOperators[$token])) {
|
||||
// We must have two operands, error if we don't
|
||||
|
@ -3677,7 +3887,10 @@ class Calculation
|
|||
case '<=': // Less than or Equal to
|
||||
case '=': // Equality
|
||||
case '<>': // Inequality
|
||||
$this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
|
||||
$result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
|
||||
break;
|
||||
// Binary Operators
|
||||
|
@ -3733,23 +3946,38 @@ class Calculation
|
|||
|
||||
break;
|
||||
case '+': // Addition
|
||||
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
|
||||
$result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
|
||||
break;
|
||||
case '-': // Subtraction
|
||||
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
|
||||
$result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
|
||||
break;
|
||||
case '*': // Multiplication
|
||||
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
|
||||
$result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
|
||||
break;
|
||||
case '/': // Division
|
||||
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
|
||||
$result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
|
||||
break;
|
||||
case '^': // Exponential
|
||||
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
|
||||
$result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
|
||||
break;
|
||||
case '&': // Concatenation
|
||||
|
@ -3782,6 +4010,10 @@ class Calculation
|
|||
$this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
|
||||
$stack->push('Value', $result);
|
||||
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
|
||||
break;
|
||||
case '|': // Intersect
|
||||
$rowIntersect = array_intersect_key($operand1, $operand2);
|
||||
|
@ -3826,6 +4058,9 @@ class Calculation
|
|||
}
|
||||
$this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
|
||||
$stack->push('Value', $result);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
} else {
|
||||
$this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
|
||||
}
|
||||
|
@ -3899,9 +4134,23 @@ class Calculation
|
|||
}
|
||||
}
|
||||
$stack->push('Value', $cellValue, $cellRef);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $cellValue;
|
||||
}
|
||||
|
||||
// if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on
|
||||
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $token, $matches)) {
|
||||
if ($pCellParent) {
|
||||
$pCell->attach($pCellParent);
|
||||
}
|
||||
if (($cellID == 'AC99') || (isset($pCell) && $pCell->getCoordinate() == 'AC99')) {
|
||||
if (defined('RESOLVING')) {
|
||||
define('RESOLVING2', true);
|
||||
} else {
|
||||
define('RESOLVING', true);
|
||||
}
|
||||
}
|
||||
|
||||
$functionName = $matches[1];
|
||||
$argCount = $stack->pop();
|
||||
$argCount = $argCount['value'];
|
||||
|
@ -3944,6 +4193,7 @@ class Calculation
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse the order of the arguments
|
||||
krsort($args);
|
||||
|
||||
|
@ -3968,21 +4218,31 @@ class Calculation
|
|||
}
|
||||
unset($arg);
|
||||
}
|
||||
|
||||
$result = call_user_func_array($functionCall, $args);
|
||||
|
||||
if ($functionName != 'MKMATRIX') {
|
||||
$this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
|
||||
}
|
||||
$stack->push('Value', self::wrapResult($result));
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $result;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// if the token is a number, boolean, string or an Excel error, push it onto the stack
|
||||
if (isset(self::$excelConstants[strtoupper($token)])) {
|
||||
$excelConstant = strtoupper($token);
|
||||
$stack->push('Constant Value', self::$excelConstants[$excelConstant]);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = self::$excelConstants[$excelConstant];
|
||||
}
|
||||
$this->debugLog->writeDebugLog('Evaluating Constant ', $excelConstant, ' as ', $this->showTypeDetails(self::$excelConstants[$excelConstant]));
|
||||
} elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
|
||||
$stack->push('Value', $token);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $token;
|
||||
}
|
||||
// if the token is a named range, push the named range name onto the stack
|
||||
} elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
|
||||
$namedRange = $matches[6];
|
||||
|
@ -3992,6 +4252,9 @@ class Calculation
|
|||
$pCell->attach($pCellParent);
|
||||
$this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
|
||||
$stack->push('Named Range', $cellValue, $namedRange);
|
||||
if (isset($storeKey)) {
|
||||
$branchStore[$storeKey] = $cellValue;
|
||||
}
|
||||
} else {
|
||||
return $this->raiseFormulaError("undefined variable '$token'");
|
||||
}
|
||||
|
@ -4053,7 +4316,7 @@ class Calculation
|
|||
* @param Stack $stack
|
||||
* @param bool $recursingArrays
|
||||
*
|
||||
* @return bool
|
||||
* @return mixed
|
||||
*/
|
||||
private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
|
||||
{
|
||||
|
@ -4090,7 +4353,7 @@ class Calculation
|
|||
// And push the result onto the stack
|
||||
$stack->push('Array', $result);
|
||||
|
||||
return true;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Simple validate the two operands if they are string values
|
||||
|
@ -4180,7 +4443,7 @@ class Calculation
|
|||
// And push the result onto the stack
|
||||
$stack->push('Value', $result);
|
||||
|
||||
return true;
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -4206,7 +4469,7 @@ class Calculation
|
|||
* @param string $matrixFunction
|
||||
* @param mixed $stack
|
||||
*
|
||||
* @return bool
|
||||
* @return bool|mixed
|
||||
*/
|
||||
private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
|
||||
{
|
||||
|
@ -4284,7 +4547,7 @@ class Calculation
|
|||
// And push the result onto the stack
|
||||
$stack->push('Value', $result);
|
||||
|
||||
return true;
|
||||
return $result;
|
||||
}
|
||||
|
||||
// trigger an error, but nicely, if need be
|
||||
|
@ -4488,4 +4751,27 @@ class Calculation
|
|||
|
||||
return $args;
|
||||
}
|
||||
|
||||
private function getUnusedBranchStoreKey()
|
||||
{
|
||||
$storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter;
|
||||
++$this->branchStoreKeyCounter;
|
||||
|
||||
return $storeKeyValue;
|
||||
}
|
||||
|
||||
private function getTokensAsString($tokens)
|
||||
{
|
||||
$tokensStr = array_map(function ($token) {
|
||||
$value = $token['value'] ?? 'no value';
|
||||
while (is_array($value)) {
|
||||
$value = array_pop($value);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}, $tokens);
|
||||
$str = '[ ' . implode(' | ', $tokensStr) . ' ]';
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -464,9 +464,10 @@ class LookupRef
|
|||
*
|
||||
* @param mixed $lookupValue The value that you want to match in lookup_array
|
||||
* @param mixed $lookupArray The range of cells being searched
|
||||
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below. If match_type is 1 or -1, the list has to be ordered.
|
||||
* @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
|
||||
* If match_type is 1 or -1, the list has to be ordered.
|
||||
*
|
||||
* @return int The relative position of the found item
|
||||
* @return int|string The relative position of the found item
|
||||
*/
|
||||
public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
|
||||
{
|
||||
|
@ -474,9 +475,10 @@ class LookupRef
|
|||
$lookupValue = Functions::flattenSingleValue($lookupValue);
|
||||
$matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);
|
||||
|
||||
$initialLookupValue = $lookupValue;
|
||||
// MATCH is not case sensitive
|
||||
// MATCH is not case sensitive, so we convert lookup value to be lower cased in case it's string type.
|
||||
if (is_string($lookupValue)) {
|
||||
$lookupValue = StringHelper::strToLower($lookupValue);
|
||||
}
|
||||
|
||||
// Lookup_value type has to be number, text, or logical values
|
||||
if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
|
||||
|
@ -522,16 +524,54 @@ class LookupRef
|
|||
// find the match
|
||||
// **
|
||||
|
||||
if ($matchType == 0 || $matchType == 1) {
|
||||
if ($matchType === 0 || $matchType === 1) {
|
||||
foreach ($lookupArray as $i => $lookupArrayValue) {
|
||||
$onlyNumeric = is_numeric($lookupArrayValue) && is_numeric($lookupValue);
|
||||
$onlyNumericExactMatch = $onlyNumeric && $lookupArrayValue == $lookupValue;
|
||||
$nonOnlyNumericExactMatch = !$onlyNumeric && $lookupArrayValue === $lookupValue;
|
||||
$exactMatch = $onlyNumericExactMatch || $nonOnlyNumericExactMatch;
|
||||
if (($matchType == 0) && $exactMatch) {
|
||||
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
|
||||
$exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
|
||||
$nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
|
||||
$exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;
|
||||
|
||||
if ($matchType === 0) {
|
||||
if ($typeMatch && is_string($lookupValue) && (bool) preg_match('/([\?\*])/', $lookupValue)) {
|
||||
$splitString = $lookupValue;
|
||||
$chars = array_map(function ($i) use ($splitString) {
|
||||
return mb_substr($splitString, $i, 1);
|
||||
}, range(0, mb_strlen($splitString) - 1));
|
||||
|
||||
$length = count($chars);
|
||||
$pattern = '/^';
|
||||
for ($j = 0; $j < $length; ++$j) {
|
||||
if ($chars[$j] === '~') {
|
||||
if (isset($chars[$j + 1])) {
|
||||
if ($chars[$j + 1] === '*') {
|
||||
$pattern .= preg_quote($chars[$j + 1], '/');
|
||||
++$j;
|
||||
} elseif ($chars[$j + 1] === '?') {
|
||||
$pattern .= preg_quote($chars[$j + 1], '/');
|
||||
++$j;
|
||||
}
|
||||
} else {
|
||||
$pattern .= preg_quote($chars[$j], '/');
|
||||
}
|
||||
} elseif ($chars[$j] === '*') {
|
||||
$pattern .= '.*';
|
||||
} elseif ($chars[$j] === '?') {
|
||||
$pattern .= '.{1}';
|
||||
} else {
|
||||
$pattern .= preg_quote($chars[$j], '/');
|
||||
}
|
||||
}
|
||||
|
||||
$pattern .= '$/';
|
||||
if ((bool) preg_match($pattern, $lookupArrayValue)) {
|
||||
// exact match
|
||||
return $i + 1;
|
||||
} elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) {
|
||||
}
|
||||
} elseif ($exactMatch) {
|
||||
// exact match
|
||||
return $i + 1;
|
||||
}
|
||||
} elseif (($matchType === 1) && $typeMatch && ($lookupArrayValue <= $lookupValue)) {
|
||||
$i = array_search($i, $keySet);
|
||||
|
||||
// The current value is the (first) match
|
||||
|
@ -539,26 +579,26 @@ class LookupRef
|
|||
}
|
||||
}
|
||||
} else {
|
||||
// matchType = -1
|
||||
|
||||
// "Special" case: since the array it's supposed to be ordered in descending order, the
|
||||
// Excel algorithm gives up immediately if the first element is smaller than the searched value
|
||||
if ($lookupArray[0] < $lookupValue) {
|
||||
return Functions::NA();
|
||||
}
|
||||
|
||||
$maxValueKey = null;
|
||||
|
||||
// The basic algorithm is:
|
||||
// Iterate and keep the highest match until the next element is smaller than the searched value.
|
||||
// Return immediately if perfect match is found
|
||||
foreach ($lookupArray as $i => $lookupArrayValue) {
|
||||
if ($lookupArrayValue == $lookupValue) {
|
||||
$typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
|
||||
$exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
|
||||
$nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
|
||||
$exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;
|
||||
|
||||
if ($exactMatch) {
|
||||
// Another "special" case. If a perfect match is found,
|
||||
// the algorithm gives up immediately
|
||||
return $i + 1;
|
||||
} elseif ($lookupArrayValue >= $lookupValue) {
|
||||
} elseif ($typeMatch & $lookupArrayValue >= $lookupValue) {
|
||||
$maxValueKey = $i + 1;
|
||||
} elseif ($typeMatch & $lookupArrayValue < $lookupValue) {
|
||||
//Excel algorithm gives up immediately if the first element is smaller than the searched value
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -36,14 +36,24 @@ class Stack
|
|||
* @param mixed $type
|
||||
* @param mixed $value
|
||||
* @param mixed $reference
|
||||
* @param null|string $storeKey will store the result under this alias
|
||||
* @param null|string $onlyIf will only run computation if the matching
|
||||
* store key is true
|
||||
* @param null|string $onlyIfNot will only run computation if the matching
|
||||
* store key is false
|
||||
*/
|
||||
public function push($type, $value, $reference = null)
|
||||
{
|
||||
$this->stack[$this->count++] = [
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
'reference' => $reference,
|
||||
];
|
||||
public function push(
|
||||
$type,
|
||||
$value,
|
||||
$reference = null,
|
||||
$storeKey = null,
|
||||
$onlyIf = null,
|
||||
$onlyIfNot = null
|
||||
) {
|
||||
$stackItem = $this->getStackItem($type, $value, $reference, $storeKey, $onlyIf, $onlyIfNot);
|
||||
|
||||
$this->stack[$this->count++] = $stackItem;
|
||||
|
||||
if ($type == 'Function') {
|
||||
$localeFunction = Calculation::localeFunc($value);
|
||||
if ($localeFunction != $value) {
|
||||
|
@ -52,6 +62,35 @@ class Stack
|
|||
}
|
||||
}
|
||||
|
||||
public function getStackItem(
|
||||
$type,
|
||||
$value,
|
||||
$reference = null,
|
||||
$storeKey = null,
|
||||
$onlyIf = null,
|
||||
$onlyIfNot = null
|
||||
) {
|
||||
$stackItem = [
|
||||
'type' => $type,
|
||||
'value' => $value,
|
||||
'reference' => $reference,
|
||||
];
|
||||
|
||||
if (isset($storeKey)) {
|
||||
$stackItem['storeKey'] = $storeKey;
|
||||
}
|
||||
|
||||
if (isset($onlyIf)) {
|
||||
$stackItem['onlyIf'] = $onlyIf;
|
||||
}
|
||||
|
||||
if (isset($onlyIfNot)) {
|
||||
$stackItem['onlyIfNot'] = $onlyIfNot;
|
||||
}
|
||||
|
||||
return $stackItem;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pop the last entry from the stack.
|
||||
*
|
||||
|
@ -90,4 +129,21 @@ class Stack
|
|||
$this->stack = [];
|
||||
$this->count = 0;
|
||||
}
|
||||
|
||||
public function __toString()
|
||||
{
|
||||
$str = 'Stack: ';
|
||||
foreach ($this->stack as $index => $item) {
|
||||
if ($index > $this->count - 1) {
|
||||
break;
|
||||
}
|
||||
$value = $item['value'] ?? 'no value';
|
||||
while (is_array($value)) {
|
||||
$value = array_pop($value);
|
||||
}
|
||||
$str .= $value . ' |> ';
|
||||
}
|
||||
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -502,10 +502,10 @@ class Html extends BaseReader
|
|||
if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
|
||||
//create merging rowspan and colspan
|
||||
$columnTo = $column;
|
||||
for ($i = 0; $i < $attributeArray['colspan'] - 1; ++$i) {
|
||||
for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
|
||||
++$columnTo;
|
||||
}
|
||||
$range = $column . $row . ':' . $columnTo . ($row + $attributeArray['rowspan'] - 1);
|
||||
$range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
|
||||
foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
|
||||
$this->rowspan[$value] = true;
|
||||
}
|
||||
|
@ -513,7 +513,7 @@ class Html extends BaseReader
|
|||
$column = $columnTo;
|
||||
} elseif (isset($attributeArray['rowspan'])) {
|
||||
//create merging rowspan
|
||||
$range = $column . $row . ':' . $column . ($row + $attributeArray['rowspan'] - 1);
|
||||
$range = $column . $row . ':' . $column . ($row + (int) $attributeArray['rowspan'] - 1);
|
||||
foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
|
||||
$this->rowspan[$value] = true;
|
||||
}
|
||||
|
@ -521,7 +521,7 @@ class Html extends BaseReader
|
|||
} elseif (isset($attributeArray['colspan'])) {
|
||||
//create merging colspan
|
||||
$columnTo = $column;
|
||||
for ($i = 0; $i < $attributeArray['colspan'] - 1; ++$i) {
|
||||
for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
|
||||
++$columnTo;
|
||||
}
|
||||
$sheet->mergeCells($column . $row . ':' . $columnTo . $row);
|
||||
|
@ -592,12 +592,6 @@ class Html extends BaseReader
|
|||
throw new Exception($pFilename . ' is an Invalid HTML file.');
|
||||
}
|
||||
|
||||
// Create new sheet
|
||||
while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
|
||||
$spreadsheet->createSheet();
|
||||
}
|
||||
$spreadsheet->setActiveSheetIndex($this->sheetIndex);
|
||||
|
||||
// Create a new DOM object
|
||||
$dom = new DOMDocument();
|
||||
// Reload the HTML file into the DOM object
|
||||
|
@ -606,14 +600,56 @@ class Html extends BaseReader
|
|||
throw new Exception('Failed to load ' . $pFilename . ' as a DOM Document');
|
||||
}
|
||||
|
||||
return $this->loadDocument($dom, $spreadsheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spreadsheet from content.
|
||||
*
|
||||
* @param string $content
|
||||
*
|
||||
* @throws Exception
|
||||
*
|
||||
* @return Spreadsheet
|
||||
*/
|
||||
public function loadFromString($content): Spreadsheet
|
||||
{
|
||||
// Create a new DOM object
|
||||
$dom = new DOMDocument();
|
||||
// Reload the HTML file into the DOM object
|
||||
$loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8'));
|
||||
if ($loaded === false) {
|
||||
throw new Exception('Failed to load content as a DOM Document');
|
||||
}
|
||||
|
||||
return $this->loadDocument($dom, new Spreadsheet());
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads PhpSpreadsheet from DOMDocument into PhpSpreadsheet instance.
|
||||
*
|
||||
* @param DOMDocument $document
|
||||
* @param Spreadsheet $spreadsheet
|
||||
*
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
*
|
||||
* @return Spreadsheet
|
||||
*/
|
||||
private function loadDocument(DOMDocument $document, Spreadsheet $spreadsheet): Spreadsheet
|
||||
{
|
||||
while ($spreadsheet->getSheetCount() <= $this->sheetIndex) {
|
||||
$spreadsheet->createSheet();
|
||||
}
|
||||
$spreadsheet->setActiveSheetIndex($this->sheetIndex);
|
||||
|
||||
// Discard white space
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$document->preserveWhiteSpace = false;
|
||||
|
||||
$row = 0;
|
||||
$column = 'A';
|
||||
$content = '';
|
||||
$this->rowspan = [];
|
||||
$this->processDomElement($dom, $spreadsheet->getActiveSheet(), $row, $column, $content);
|
||||
$this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
|
||||
|
||||
// Return
|
||||
return $spreadsheet;
|
||||
|
|
|
@ -23,8 +23,8 @@ class NumberFormat extends Supervisor
|
|||
const FORMAT_PERCENTAGE_00 = '0.00%';
|
||||
|
||||
const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd';
|
||||
const FORMAT_DATE_YYYYMMDD = 'yy-mm-dd';
|
||||
const FORMAT_DATE_DDMMYYYY = 'dd/mm/yy';
|
||||
const FORMAT_DATE_YYYYMMDD = 'yyyy-mm-dd';
|
||||
const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy';
|
||||
const FORMAT_DATE_DMYSLASH = 'd/m/yy';
|
||||
const FORMAT_DATE_DMYMINUS = 'd-m-yy';
|
||||
const FORMAT_DATE_DMMINUS = 'd-m';
|
||||
|
@ -43,7 +43,7 @@ class NumberFormat extends Supervisor
|
|||
const FORMAT_DATE_TIME6 = 'h:mm:ss';
|
||||
const FORMAT_DATE_TIME7 = 'i:s.S';
|
||||
const FORMAT_DATE_TIME8 = 'h:mm:ss;@';
|
||||
const FORMAT_DATE_YYYYMMDDSLASH = 'yy/mm/dd;@';
|
||||
const FORMAT_DATE_YYYYMMDDSLASH = 'yyyy/mm/dd;@';
|
||||
|
||||
const FORMAT_CURRENCY_USD_SIMPLE = '"$"#,##0.00_-';
|
||||
const FORMAT_CURRENCY_USD = '$#,##0_-';
|
||||
|
|
|
@ -1441,7 +1441,7 @@ class Worksheet implements IComparable
|
|||
$this->parent->setActiveSheetIndex($this->parent->getIndex($this));
|
||||
|
||||
// set cell coordinate as active
|
||||
$this->setSelectedCells(strtoupper($pCellCoordinate));
|
||||
$this->setSelectedCells($pCellCoordinate);
|
||||
|
||||
return $this->parent->getCellXfSupervisor();
|
||||
}
|
||||
|
@ -2115,6 +2115,10 @@ class Worksheet implements IComparable
|
|||
public function removeRow($pRow, $pNumRows = 1)
|
||||
{
|
||||
if ($pRow >= 1) {
|
||||
for ($r = 0; $r < $pNumRows; ++$r) {
|
||||
$this->getCellCollection()->removeRow($pRow + $r);
|
||||
}
|
||||
|
||||
$highestRow = $this->getHighestDataRow();
|
||||
$objReferenceHelper = ReferenceHelper::getInstance();
|
||||
$objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this);
|
||||
|
|
|
@ -891,8 +891,8 @@ class Html extends BaseWriter
|
|||
$css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
|
||||
|
||||
if ($columnDimension->getVisible() === false) {
|
||||
$css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse';
|
||||
$css['table.sheet' . $sheetIndex . ' col.col' . $column]['*display'] = 'none'; // target IE6+7
|
||||
$css['table.sheet' . $sheetIndex . ' .column' . $column]['visibility'] = 'collapse';
|
||||
$css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none'; // target IE6+7
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -163,4 +163,195 @@ class CalculationTest extends TestCase
|
|||
|
||||
self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
|
||||
}
|
||||
|
||||
public function testCellWithFormulaTwoIndirect()
|
||||
{
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$workSheet = $spreadsheet->getActiveSheet();
|
||||
$cell1 = $workSheet->getCell('A1');
|
||||
$cell1->setValue('2');
|
||||
$cell2 = $workSheet->getCell('B1');
|
||||
$cell2->setValue('3');
|
||||
$cell2 = $workSheet->getCell('C1');
|
||||
$cell2->setValue('4');
|
||||
$cell3 = $workSheet->getCell('D1');
|
||||
$cell3->setValue('=SUM(INDIRECT("A"&ROW()),INDIRECT("B"&ROW()),INDIRECT("C"&ROW()))');
|
||||
|
||||
self::assertEquals('9', $cell3->getCalculatedValue());
|
||||
}
|
||||
|
||||
public function testBranchPruningFormulaParsingSimpleCase()
|
||||
{
|
||||
$calculation = Calculation::getInstance();
|
||||
$calculation->flushInstance(); // resets the ids
|
||||
|
||||
// Very simple formula
|
||||
$formula = '=IF(A1="please +",B1)';
|
||||
$tokens = $calculation->parseFormula($formula);
|
||||
|
||||
$foundEqualAssociatedToStoreKey = false;
|
||||
$foundConditionalOnB1 = false;
|
||||
foreach ($tokens as $token) {
|
||||
$isBinaryOperator = $token['type'] == 'Binary Operator';
|
||||
$isEqual = $token['value'] == '=';
|
||||
$correctStoreKey = ($token['storeKey'] ?? '') == 'storeKey-0';
|
||||
$correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-0';
|
||||
$isB1Reference = ($token['reference'] ?? '') == 'B1';
|
||||
|
||||
$foundEqualAssociatedToStoreKey = $foundEqualAssociatedToStoreKey ||
|
||||
($isBinaryOperator && $isEqual && $correctStoreKey);
|
||||
|
||||
$foundConditionalOnB1 = $foundConditionalOnB1 ||
|
||||
($isB1Reference && $correctOnlyIf);
|
||||
}
|
||||
$this->assertTrue($foundEqualAssociatedToStoreKey);
|
||||
$this->assertTrue($foundConditionalOnB1);
|
||||
}
|
||||
|
||||
public function testBranchPruningFormulaParsingMultipleIfsCase()
|
||||
{
|
||||
$calculation = Calculation::getInstance();
|
||||
$calculation->flushInstance(); // resets the ids
|
||||
|
||||
//
|
||||
// Internal operation
|
||||
$formula = '=IF(A1="please +",SUM(B1:B3))+IF(A2="please *",PRODUCT(C1:C3), C1)';
|
||||
$tokens = $calculation->parseFormula($formula);
|
||||
|
||||
$plusGotTagged = false;
|
||||
$productFunctionCorrectlyTagged = false;
|
||||
foreach ($tokens as $token) {
|
||||
$isBinaryOperator = $token['type'] == 'Binary Operator';
|
||||
$isPlus = $token['value'] == '+';
|
||||
$anyStoreKey = isset($token['storeKey']);
|
||||
$anyOnlyIf = isset($token['onlyIf']);
|
||||
$anyOnlyIfNot = isset($token['onlyIfNot']);
|
||||
$plusGotTagged = $plusGotTagged ||
|
||||
($isBinaryOperator && $isPlus &&
|
||||
($anyStoreKey || $anyOnlyIfNot || $anyOnlyIf));
|
||||
|
||||
$isFunction = $token['type'] == 'Function';
|
||||
$isProductFunction = $token['value'] == 'PRODUCT(';
|
||||
$correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-1';
|
||||
$productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isFunction && $isProductFunction && $correctOnlyIf);
|
||||
}
|
||||
$this->assertFalse($plusGotTagged, 'chaining IF( should not affect the external operators');
|
||||
$this->assertTrue($productFunctionCorrectlyTagged, 'function nested inside if should be tagged to be processed only if parent branching requires it');
|
||||
}
|
||||
|
||||
public function testBranchPruningFormulaParingNestedIfCase()
|
||||
{
|
||||
$calculation = Calculation::getInstance();
|
||||
$calculation->flushInstance(); // resets the ids
|
||||
|
||||
$formula = '=IF(A1="please +",SUM(B1:B3),1+IF(NOT(A2="please *"),C2-C1,PRODUCT(C1:C3)))';
|
||||
$tokens = $calculation->parseFormula($formula);
|
||||
|
||||
$plusCorrectlyTagged = false;
|
||||
$productFunctionCorrectlyTagged = false;
|
||||
$notFunctionCorrectlyTagged = false;
|
||||
$findOneOperandCountTagged = false;
|
||||
foreach ($tokens as $token) {
|
||||
$value = $token['value'];
|
||||
$isPlus = $value == '+';
|
||||
$isProductFunction = $value == 'PRODUCT(';
|
||||
$isNotFunction = $value == 'NOT(';
|
||||
$isIfOperand = $token['type'] == 'Operand Count for Function IF()';
|
||||
$isOnlyIfNotDepth1 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-1';
|
||||
$isStoreKeyDepth1 = (array_key_exists('storeKey', $token) ? $token['storeKey'] : null) == 'storeKey-1';
|
||||
$isOnlyIfNotDepth0 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-0';
|
||||
|
||||
$plusCorrectlyTagged = $plusCorrectlyTagged || ($isPlus && $isOnlyIfNotDepth0);
|
||||
$notFunctionCorrectlyTagged = $notFunctionCorrectlyTagged || ($isNotFunction && $isOnlyIfNotDepth0 && $isStoreKeyDepth1);
|
||||
$productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isProductFunction && $isOnlyIfNotDepth1 && !$isStoreKeyDepth1 && !$isOnlyIfNotDepth0);
|
||||
$findOneOperandCountTagged = $findOneOperandCountTagged || ($isIfOperand && $isOnlyIfNotDepth0);
|
||||
}
|
||||
$this->assertTrue($plusCorrectlyTagged);
|
||||
$this->assertTrue($productFunctionCorrectlyTagged);
|
||||
$this->assertTrue($notFunctionCorrectlyTagged);
|
||||
}
|
||||
|
||||
public function testBranchPruningFormulaParsingNoArgumentFunctionCase()
|
||||
{
|
||||
$calculation = Calculation::getInstance();
|
||||
$calculation->flushInstance(); // resets the ids
|
||||
|
||||
$formula = '=IF(AND(TRUE(),A1="please +"),2,3)';
|
||||
// this used to raise a parser error, we keep it even though we don't
|
||||
// test the output
|
||||
$calculation->parseFormula($formula);
|
||||
}
|
||||
|
||||
public function testBranchPruningFormulaParsingInequalitiesConditionsCase()
|
||||
{
|
||||
$calculation = Calculation::getInstance();
|
||||
$calculation->flushInstance(); // resets the ids
|
||||
|
||||
$formula = '=IF(A1="flag",IF(A2<10, 0) + IF(A3<10000, 0))';
|
||||
$tokens = $calculation->parseFormula($formula);
|
||||
$properlyTaggedPlus = false;
|
||||
foreach ($tokens as $token) {
|
||||
$isPlus = $token['value'] === '+';
|
||||
$hasOnlyIf = !empty($token['onlyIf']);
|
||||
|
||||
$properlyTaggedPlus = $properlyTaggedPlus ||
|
||||
($isPlus && $hasOnlyIf);
|
||||
}
|
||||
$this->assertTrue($properlyTaggedPlus);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $expectedResult
|
||||
* @param $dataArray
|
||||
* @param string $formula
|
||||
* @param string $cellCoordinates where to put the formula
|
||||
* @param string[] $shouldBeSetInCacheCells coordinates of cells that must
|
||||
* be set in cache
|
||||
* @param string[] $shouldNotBeSetInCacheCells coordinates of cells that must
|
||||
* not be set in cache because of pruning
|
||||
*
|
||||
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||
* @dataProvider dataProviderBranchPruningFullExecution
|
||||
*/
|
||||
public function testFullExecution(
|
||||
$expectedResult,
|
||||
$dataArray,
|
||||
$formula,
|
||||
$cellCoordinates,
|
||||
$shouldBeSetInCacheCells = [],
|
||||
$shouldNotBeSetInCacheCells = []
|
||||
) {
|
||||
$spreadsheet = new Spreadsheet();
|
||||
$sheet = $spreadsheet->getActiveSheet();
|
||||
|
||||
$sheet->fromArray($dataArray);
|
||||
$cell = $sheet->getCell($cellCoordinates);
|
||||
$calculation = Calculation::getInstance($cell->getWorksheet()->getParent());
|
||||
|
||||
$cell->setValue($formula);
|
||||
$calculated = $cell->getCalculatedValue();
|
||||
$this->assertEquals($expectedResult, $calculated);
|
||||
|
||||
// this mostly to ensure that at least some cells are cached
|
||||
foreach ($shouldBeSetInCacheCells as $setCell) {
|
||||
unset($inCache);
|
||||
$calculation->getValueFromCache('Worksheet!' . $setCell, $inCache);
|
||||
$this->assertNotEmpty($inCache);
|
||||
}
|
||||
|
||||
foreach ($shouldNotBeSetInCacheCells as $notSetCell) {
|
||||
unset($inCache);
|
||||
$calculation->getValueFromCache('Worksheet!' . $notSetCell, $inCache);
|
||||
$this->assertEmpty($inCache);
|
||||
}
|
||||
|
||||
$calculation->disableBranchPruning();
|
||||
$calculated = $cell->getCalculatedValue();
|
||||
$this->assertEquals($expectedResult, $calculated);
|
||||
}
|
||||
|
||||
public function dataProviderBranchPruningFullExecution()
|
||||
{
|
||||
return require 'data/Calculation/Calculation.php';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -299,6 +299,36 @@ class HtmlTest extends TestCase
|
|||
unlink($filename);
|
||||
}
|
||||
|
||||
public function testCanLoadFromString()
|
||||
{
|
||||
$html = '<table>
|
||||
<tr>
|
||||
<td>Hello World</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hello<br />World</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Hello<br>World</td>
|
||||
</tr>
|
||||
</table>';
|
||||
$spreadsheet = (new Html())->loadFromString($html);
|
||||
$firstSheet = $spreadsheet->getSheet(0);
|
||||
|
||||
$cellStyle = $firstSheet->getStyle('A1');
|
||||
self::assertFalse($cellStyle->getAlignment()->getWrapText());
|
||||
|
||||
$cellStyle = $firstSheet->getStyle('A2');
|
||||
self::assertTrue($cellStyle->getAlignment()->getWrapText());
|
||||
$cellValue = $firstSheet->getCell('A2')->getValue();
|
||||
$this->assertContains("\n", $cellValue);
|
||||
|
||||
$cellStyle = $firstSheet->getStyle('A3');
|
||||
self::assertTrue($cellStyle->getAlignment()->getWrapText());
|
||||
$cellValue = $firstSheet->getCell('A3')->getValue();
|
||||
$this->assertContains("\n", $cellValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $html
|
||||
*
|
||||
|
@ -321,4 +351,14 @@ class HtmlTest extends TestCase
|
|||
{
|
||||
return (new Html())->load($filename);
|
||||
}
|
||||
|
||||
public function testRowspanInRendering()
|
||||
{
|
||||
$filename = './data/Reader/HTML/rowspan.html';
|
||||
$reader = new Html();
|
||||
$spreadsheet = $reader->load($filename);
|
||||
|
||||
$actual = $spreadsheet->getActiveSheet()->getMergeCells();
|
||||
self::assertSame(['A2:C2' => 'A2:C2'], $actual);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheetTests;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class SpreadsheetTest extends TestCase
|
||||
{
|
||||
/** @var Spreadsheet */
|
||||
private $object;
|
||||
|
||||
public function setUp()
|
||||
{
|
||||
parent::setUp();
|
||||
$this->object = new Spreadsheet();
|
||||
$sheet = $this->object->getActiveSheet();
|
||||
|
||||
$sheet->setTitle('someSheet1');
|
||||
$sheet = new Worksheet();
|
||||
$sheet->setTitle('someSheet2');
|
||||
$this->object->addSheet($sheet);
|
||||
$sheet = new Worksheet();
|
||||
$sheet->setTitle('someSheet 3');
|
||||
$this->object->addSheet($sheet);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function dataProviderForSheetNames()
|
||||
{
|
||||
$array = [
|
||||
[0, 'someSheet1'],
|
||||
[0, "'someSheet1'"],
|
||||
[1, 'someSheet2'],
|
||||
[1, "'someSheet2'"],
|
||||
[2, 'someSheet 3'],
|
||||
[2, "'someSheet 3'"],
|
||||
];
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $index
|
||||
* @param $sheetName
|
||||
*
|
||||
* @dataProvider dataProviderForSheetNames
|
||||
*/
|
||||
public function testGetSheetByName($index, $sheetName)
|
||||
{
|
||||
$this->assertEquals($this->object->getSheet($index), $this->object->getSheetByName($sheetName));
|
||||
}
|
||||
}
|
|
@ -138,6 +138,10 @@ class WorksheetTest extends TestCase
|
|||
['testTitle!B2', 'testTitle', 'B2', 'B2'],
|
||||
['test!Title!B2', 'test!Title', 'B2', 'B2'],
|
||||
['test Title!B2', 'test Title', 'B2', 'B2'],
|
||||
['test!Title!B2', 'test!Title', 'B2', 'B2'],
|
||||
["'testSheet 1'!A3", "'testSheet 1'", 'A3', 'A3'],
|
||||
["'testSheet1'!A2", "'testSheet1'", 'A2', 'A2'],
|
||||
["'testSheet 2'!A1", "'testSheet 2'", 'A1', 'A1'],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -157,4 +161,24 @@ class WorksheetTest extends TestCase
|
|||
self::assertSame($expectTitle, $arRange[0]);
|
||||
self::assertSame($expectCell2, $arRange[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix https://github.com/PHPOffice/PhpSpreadsheet/issues/868 when cells are not removed correctly
|
||||
* on row deletion.
|
||||
*/
|
||||
public function testRemoveCellsCorrectlyWhenRemovingRow()
|
||||
{
|
||||
$workbook = new Spreadsheet();
|
||||
$worksheet = $workbook->getActiveSheet();
|
||||
$worksheet->getCell('A2')->setValue('A2');
|
||||
$worksheet->getCell('C1')->setValue('C1');
|
||||
$worksheet->removeRow(1);
|
||||
$this->assertEquals(
|
||||
'A2',
|
||||
$worksheet->getCell('A1')->getValue()
|
||||
);
|
||||
$this->assertNull(
|
||||
$worksheet->getCell('C1')->getValue()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
function calculationTestDataGenerator()
|
||||
{
|
||||
$dataArray1 = [
|
||||
['please +', 'please *', 'increment'],
|
||||
[1, 1, 1], // sum is 3
|
||||
[3, 3, 3], // product is 27
|
||||
];
|
||||
$set0 = [3, $dataArray1, '=IF(A1="please +", SUM(A2:C2), 2)', 'E5'];
|
||||
|
||||
$set1 = [3, $dataArray1, '=IF(TRUE(), SUM(A2:C2), 2)', 'E5'];
|
||||
|
||||
$formula1 = '=IF(A1="please +",SUM(A2:C2),7 + IF(B1="please *", 4, 2))';
|
||||
$set2 = [3, $dataArray1, $formula1, 'E5'];
|
||||
|
||||
$dataArray1[0][0] = 'not please + something else';
|
||||
$set3 = [11, $dataArray1, $formula1, 'E5'];
|
||||
|
||||
$dataArray2 = [
|
||||
['flag1', 'flag2', 'flag3', 'flag1'],
|
||||
[1, 2, 3, 4],
|
||||
[5, 6, 7, 8],
|
||||
];
|
||||
$set4 = [3, $dataArray2, '=IF($A$1=$B$1,A2,IF($A$1=$C$1,B2,IF($A$1=$D$1,C2,C3)))', 'E5'];
|
||||
|
||||
$dataArray2[0][0] = 'flag3';
|
||||
$set5 = [2, $dataArray2, '=IF(A1=B1,A2,IF(A1=C1,B2,IF(A1=D1,C2,C3)))', 'E5'];
|
||||
|
||||
$dataArray3 = [
|
||||
[1, 2, 3],
|
||||
[4, 5, 6],
|
||||
[7, 8, 9],
|
||||
];
|
||||
$set6 = [0, $dataArray3, '=IF(A1+B1>3,C1,0)', 'E5'];
|
||||
|
||||
$dataArray4 = [
|
||||
['noflag', 0, 0],
|
||||
[127000, 0, 0],
|
||||
[10000, 0.03, 0],
|
||||
[20000, 0.06, 0],
|
||||
[40000, 0.09, 0],
|
||||
[70000, 0.12, 0],
|
||||
[90000, 0.03, 0],
|
||||
];
|
||||
$formula2 = '=IF(A1="flag",IF(A2<10, 0) + IF(A3<10000, 0))';
|
||||
$set7 = [false, $dataArray4, $formula2, 'E5'];
|
||||
|
||||
$dataArray5 = [
|
||||
[1, 2],
|
||||
[3, 4],
|
||||
['=A1+A2', '=SUM(B1:B2)'],
|
||||
['take A', 0],
|
||||
];
|
||||
$formula3 = '=IF(A4="take A", A3, B3)';
|
||||
$set8 = [4, $dataArray5, $formula3, 'E5', ['A3'], ['B3']];
|
||||
|
||||
return [$set0, $set1, $set2, $set3, $set4, $set5, $set6, $set7, $set8];
|
||||
}
|
||||
|
||||
return calculationTestDataGenerator();
|
|
@ -102,7 +102,147 @@ return [
|
|||
3,
|
||||
'x',
|
||||
[[0], [0], ['x'], ['x'], ['x']],
|
||||
0
|
||||
0,
|
||||
],
|
||||
[
|
||||
2,
|
||||
'a',
|
||||
[false, 'a', 1],
|
||||
-1,
|
||||
],
|
||||
[
|
||||
'#N/A', // Expected
|
||||
0,
|
||||
['x', true, false],
|
||||
-1,
|
||||
],
|
||||
[
|
||||
'#N/A', // Expected
|
||||
true,
|
||||
['a', 'b', 'c'],
|
||||
-1,
|
||||
],
|
||||
[
|
||||
'#N/A', // Expected
|
||||
true,
|
||||
[0, 1, 2],
|
||||
-1,
|
||||
],
|
||||
[
|
||||
'#N/A', // Expected
|
||||
true,
|
||||
[0, 1, 2],
|
||||
0,
|
||||
],
|
||||
[
|
||||
'#N/A', // Expected
|
||||
true,
|
||||
[0, 1, 2],
|
||||
1,
|
||||
],
|
||||
[
|
||||
1, // Expected
|
||||
true,
|
||||
[true, true, true],
|
||||
-1,
|
||||
],
|
||||
[
|
||||
1, // Expected
|
||||
true,
|
||||
[true, true, true],
|
||||
0,
|
||||
],
|
||||
[
|
||||
3, // Expected
|
||||
true,
|
||||
[true, true, true],
|
||||
1,
|
||||
],
|
||||
// lookup stops when value < searched one
|
||||
[
|
||||
5, // Expected
|
||||
6,
|
||||
[true, false, 'a', 'z', 222222, 2, 99999999],
|
||||
-1,
|
||||
],
|
||||
// if element of same data type met and it is < than searched one #N/A - no further processing
|
||||
[
|
||||
'#N/A', // Expected
|
||||
6,
|
||||
[true, false, 'a', 'z', 2, 888],
|
||||
-1,
|
||||
],
|
||||
[
|
||||
'#N/A', // Expected
|
||||
6,
|
||||
['6'],
|
||||
-1,
|
||||
],
|
||||
// expression match
|
||||
[
|
||||
2, // Expected
|
||||
'a?b',
|
||||
['a', 'abb', 'axc'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
1, // Expected
|
||||
'a*',
|
||||
['aAAAAAAA', 'as', 'az'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
3, // Expected
|
||||
'1*11*1',
|
||||
['abc', 'efh', '1a11b1'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
3, // Expected
|
||||
'1*11*1',
|
||||
['abc', 'efh', '1a11b1'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
2, // Expected
|
||||
'a*~*c',
|
||||
['aAAAAA', 'a123456*c', 'az'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
3, // Expected
|
||||
'a*123*b',
|
||||
['aAAAAA', 'a123456*c', 'a99999123b'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
1, // Expected
|
||||
'*',
|
||||
['aAAAAA', 'a111123456*c', 'qq'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
2, // Expected
|
||||
'?',
|
||||
['aAAAAA', 'a', 'a99999123b'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
'#N/A', // Expected
|
||||
'?',
|
||||
[1, 22, 333],
|
||||
0,
|
||||
],
|
||||
[
|
||||
3, // Expected
|
||||
'???',
|
||||
[1, 22, 'aaa'],
|
||||
0,
|
||||
],
|
||||
[
|
||||
3, // Expected
|
||||
'*',
|
||||
[1, 22, 'aaa'],
|
||||
0,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>A1</td>
|
||||
<td>B1</td>
|
||||
<td>C1</td>
|
||||
<td>D1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan='3"'>A2 with invalid colspan</td>
|
||||
<td>D2<td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
Loading…
Reference in New Issue