Merge branch 'master' into Further-Test-Refactoring

This commit is contained in:
Adrien Crivelli 2019-09-20 16:04:36 -07:00
commit ee5134a954
No known key found for this signature in database
GPG Key ID: B182FD79DC6DE92E
16 changed files with 1177 additions and 180 deletions

View File

@ -9,33 +9,55 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### Added
- Implementation of IFNA() Logical Function - 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) ### Fixed
- 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)
- ...
## [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 ### Fixed
- Fix to AVERAGEIF() function when called with a third argument - 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) - Eliminate duplicate fill none style entries [#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 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 [Issue #1009](https://github.com/PHPOffice/PhpSpreadsheet/issues/1009) - 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 [Issue #1046](https://github.com/PHPOffice/PhpSpreadsheet/issues/1046) - 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 - [Issue #1020](https://github.com/PHPOffice/PhpSpreadsheet/issues/1020) and [PR #1021](https://github.com/PHPOffice/PhpSpreadsheet/pull/1021) - 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 - 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 ## [1.8.2] - 2019-07-08
### Fixed ### Fixed
- Uncaught error when opening ods file and properties aren't defined - [Issue #1047](https://github.com/PHPOffice/PhpSpreadsheet/issues/1047) - Uncaught error when opening ods file and properties aren't defined [#1047](https://github.com/PHPOffice/PhpSpreadsheet/issues/1047)
- Xlsx Reader Cell datavalidations bug - [PR #1052](https://github.com/PHPOffice/PhpSpreadsheet/pull/1052) - Xlsx Reader Cell datavalidations bug [#1052](https://github.com/PHPOffice/PhpSpreadsheet/pull/1052)
## [1.8.1] - 2019-07-02 ## [1.8.1] - 2019-07-02
### Fixed ### 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 ## [1.8.0] - 2019-07-01
@ -51,7 +73,7 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### 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) - Add accounting number format style [#974](https://github.com/PHPOffice/PhpSpreadsheet/pull/974)
### Fixed ### Fixed
@ -79,24 +101,24 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Added ### Added
- Refactored Matrix Functions to use external Matrix library - 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 ### Fixed
- Improve XLSX parsing speed if no readFilter is applied - [#772](https://github.com/PHPOffice/PhpSpreadsheet/issues/772) - 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) - 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) - 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 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) - 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) - 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) - 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 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 [#825](https://github.com/PHPOffice/PhpSpreadsheet/pull/825)
- Improved performance when loading large spreadsheets - [#824](https://github.com/PHPOffice/PhpSpreadsheet/pull/824) - 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 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 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 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) - Fix iconv unsupported `//IGNORE//TRANSLIT` on IBM i [#791](https://github.com/PHPOffice/PhpSpreadsheet/issues/791)
### Changed ### Changed
@ -106,59 +128,59 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Security ### 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 ## [1.5.1] - 2018-11-20
### Security ### 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 ### 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 ### Fixed
- Support numeric condition in SUMIF, SUMIFS, AVERAGEIF, COUNTIF, MAXIF and MINIF - [#683](https://github.com/PHPOffice/PhpSpreadsheet/issues/683) - 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) - 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) - 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) - 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) - 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) - 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) - Fix index overflow on data values array [#748](https://github.com/PHPOffice/PhpSpreadsheet/pull/748)
## [1.5.0] - 2018-10-21 ## [1.5.0] - 2018-10-21
### Added ### Added
- PHP 7.3 support - 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 ### Fixed
- Sheet title can contain exclamation mark - [#325](https://github.com/PHPOffice/PhpSpreadsheet/issues/325) - 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) - 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) - 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) - 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) - Correctly determine delimiter when CSV contains line breaks inside enclosures [#716](https://github.com/PHPOffice/PhpSpreadsheet/issues/716)
## [1.4.1] - 2018-09-30 ## [1.4.1] - 2018-09-30
### Fixed ### Fixed
- Remove locale from formatting string - [#644](https://github.com/PHPOffice/PhpSpreadsheet/pull/644) - 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) - 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 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) - 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 ## [1.4.0] - 2018-08-06
### Added ### Added
- Add excel function EXACT(value1, value2) support - [#595](https://github.com/PHPOffice/PhpSpreadsheet/pull/595) - 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) - 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) - 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 - Added calculation engine support for the new bitwise functions that were added in MS Excel 2013
- BITAND() Returns a Bitwise 'And' of two numbers - BITAND() Returns a Bitwise 'And' of two numbers
- BITOR() Returns a Bitwise 'Or' of two number - BITOR() Returns a Bitwise 'Or' of two number
@ -207,10 +229,10 @@ and this project adheres to [Semantic Versioning](https://semver.org).
### Fixed ### Fixed
- Fix ISFORMULA() function to work with a cell reference to another worksheet - 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) - 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) - 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) - 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) - 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) - 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) - 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) - 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 ### 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 ## [1.3.0] - 2018-06-10
### Added ### 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) - 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) - 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) - Allow escape character to be set in CSV reader [#492](https://github.com/PHPOffice/PhpSpreadsheet/issues/492)
### Fixed ### 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) - 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) - `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 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) - 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) - 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) - `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) - 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) - Corruption errors for saved Xlsx docs with frozen panes [#532](https://github.com/PHPOffice/PhpSpreadsheet/issues/532)
## [1.2.1] - 2018-04-10 ## [1.2.1] - 2018-04-10
### Fixed ### 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 ## [1.2.0] - 2018-03-04
### Added ### Added
- HTML writer creates a generator meta tag - [#312](https://github.com/PHPOffice/PhpSpreadsheet/issues/312) - 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 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) - Support for `_xlfn.` prefixed functions and `ISFORMULA`, `MODE.SNGL`, `STDEV.S`, `STDEV.P` [#390](https://github.com/PHPOffice/PhpSpreadsheet/pull/390)
### Fixed ### Fixed
- Avoid potentially unsupported PSR-16 cache keys - [#354](https://github.com/PHPOffice/PhpSpreadsheet/issues/354) - 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) - 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) - 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) - 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) - 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) - 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) - `setStrikethrough()` did not set the font [#403](https://github.com/PHPOffice/PhpSpreadsheet/issues/403)
## [1.1.0] - 2018-01-28 ## [1.1.0] - 2018-01-28
### Added ### Added
- Support for PHP 7.2 - Support for PHP 7.2
- Support cell comments in HTML writer and reader - [#308](https://github.com/PHPOffice/PhpSpreadsheet/issues/308) - 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) - 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 for line width for data series when rendering Xlsx [#329](https://github.com/PHPOffice/PhpSpreadsheet/pull/329)
### Fixed ### Fixed
- Better auto-detection of CSV separators - [#305](https://github.com/PHPOffice/PhpSpreadsheet/issues/305) - 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) - 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) - 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) - `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) - 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) - Cell coordinates which are already a range cause an exception [#319](https://github.com/PHPOffice/PhpSpreadsheet/issues/319)
## [1.0.0] - 2017-12-25 ## [1.0.0] - 2017-12-25
### Added ### Added
- Support to write merged cells in ODS format - [#287](https://github.com/PHPOffice/PhpSpreadsheet/issues/287) - 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) - Able to set the `topLeftCell` in freeze panes [#261](https://github.com/PHPOffice/PhpSpreadsheet/pull/261)
- Support `DateTimeImmutable` as cell value - Support `DateTimeImmutable` as cell value
- Support migration of prefixed classes - Support migration of prefixed classes
### Fixed ### Fixed
- Can read very small HTML files - [#194](https://github.com/PHPOffice/PhpSpreadsheet/issues/194) - 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) - 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) - Date format compatible with both LibreOffice and Excel [#298](https://github.com/PHPOffice/PhpSpreadsheet/issues/298)
### BREAKING CHANGE ### 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) - 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) - 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 ### 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) - 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) - `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 ### BREAKING CHANGE

View File

@ -173,10 +173,9 @@ code:
$writer->setOffice2003Compatibility(true); $writer->setOffice2003Compatibility(true);
$writer->save("05featuredemo.xlsx"); $writer->save("05featuredemo.xlsx");
**Office2003 compatibility should only be used when needed** Office2003 **Office2003 compatibility option should only be used when needed** because
compatibility option should only be used when needed. This option it disables several Office2007 file format options, resulting in a
disables several Office2007 file format options, resulting in a lower-featured Office2007 spreadsheet.
lower-featured Office2007 spreadsheet when this option is used.
## Excel 5 (BIFF) file format ## 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. 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');
```

View File

@ -66,6 +66,15 @@ class Calculation
*/ */
private $calculationCacheEnabled = true; 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 * List of operators that can be used within formulae
* The true/false value indicates whether it is a binary operator or a unary operator. * The true/false value indicates whether it is a binary operator or a unary operator.
@ -2256,6 +2265,7 @@ class Calculation
public function flushInstance() public function flushInstance()
{ {
$this->clearCalculationCache(); $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. * Get the currently defined locale code.
* *
@ -2867,6 +2903,7 @@ class Calculation
if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) { if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) {
$this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache'); $this->debugLog->writeDebugLog('Retrieving value for cell ', $cellReference, ' from cache');
// Return the cached result // Return the cached result
$cellValue = $this->calculationCache[$cellReference]; $cellValue = $this->calculationCache[$cellReference];
return true; return true;
@ -3326,9 +3363,53 @@ class Calculation
// - is a negation or + is a positive operator rather than an operation // - 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 $expectingOperand = false; // We use this test in syntax-checking the expression to determine whether an operand
// should be null in a function call // 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 // The guts of the lexical parser
// Loop through the formula extracting each operator and operand in turn // Loop through the formula extracting each operator and operand in turn
while (true) { 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 $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]]))) { if ((isset(self::$comparisonOperators[$opCharacter])) && (strlen($formula) > $index) && (isset(self::$comparisonOperators[$formula[$index + 1]]))) {
$opCharacter .= $formula[++$index]; $opCharacter .= $formula[++$index];
@ -3338,10 +3419,12 @@ class Calculation
$isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match); $isOperandOrFunction = preg_match($regexpMatchString, substr($formula, $index), $match);
if ($opCharacter == '-' && !$expectingOperator) { // Is it a negation instead of a minus? 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 ++$index; // and drop the negation symbol
} elseif ($opCharacter == '%' && $expectingOperator) { } 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; ++$index;
} elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded? } elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded?
++$index; // Drop the redundant plus symbol ++$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']])) { @(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 $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; ++$index;
$expectingOperator = false; $expectingOperator = false;
} elseif ($opCharacter == ')' && $expectingOperator) { // Are we expecting to close a parenthesis? } elseif ($opCharacter == ')' && $expectingOperator) { // Are we expecting to close a parenthesis?
@ -3366,7 +3452,29 @@ class Calculation
$output[] = $o2; $output[] = $o2;
} }
$d = $stack->last(2); $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 (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 $functionName = $matches[1]; // Get the function name
$d = $stack->pop(); $d = $stack->pop();
$argumentCount = $d['value']; // See how many arguments there were (argument count is the next value stored on the stack) $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; ++$index;
} elseif ($opCharacter == ',') { // Is this the separator for function arguments? } 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 ( while (($o2 = $stack->pop()) && $o2['value'] != '(') { // Pop off the stack back to the last (
if ($o2 === null) { if ($o2 === null) {
return $this->raiseFormulaError('Formula Error: Unexpected ,'); return $this->raiseFormulaError('Formula Error: Unexpected ,');
@ -3444,13 +3566,19 @@ class Calculation
return $this->raiseFormulaError('Formula Error: Unexpected ,'); return $this->raiseFormulaError('Formula Error: Unexpected ,');
} }
$d = $stack->pop(); $d = $stack->pop();
$stack->push($d['type'], ++$d['value'], $d['reference']); // increment the argument count $itemStoreKey = $d['storeKey'] ?? null;
$stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again $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; $expectingOperator = false;
$expectingOperand = true; $expectingOperand = true;
++$index; ++$index;
} elseif ($opCharacter == '(' && !$expectingOperator) { } 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; ++$index;
} elseif ($isOperandOrFunction && !$expectingOperator) { // do we now have a function/variable/number? } elseif ($isOperandOrFunction && !$expectingOperator) { // do we now have a function/variable/number?
$expectingOperator = true; $expectingOperator = true;
@ -3461,13 +3589,28 @@ class Calculation
if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) { if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/i', $val, $matches)) {
$val = preg_replace('/\s/u', '', $val); $val = preg_replace('/\s/u', '', $val);
if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function 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); $ax = preg_match('/^\s*(\s*\))/ui', substr($formula, $index + $length), $amatch);
if ($ax) { 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; $expectingOperator = true;
} else { } else {
$stack->push('Operand Count for Function ' . strtoupper($val) . ')', 1); $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
$expectingOperator = false; $expectingOperator = false;
} }
$stack->push('Brace', '('); $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 } 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 // If the last entry on the stack was a : operator, then we may have a row or column range reference
$testPrevOp = $stack->last(1); $testPrevOp = $stack->last(1);
@ -3542,7 +3687,7 @@ class Calculation
} elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) { } elseif (($localeConstant = array_search(trim(strtoupper($val)), self::$localeBoolean)) !== false) {
$val = self::$excelConstants[$localeConstant]; $val = self::$excelConstants[$localeConstant];
} }
$details = ['type' => 'Value', 'value' => $val, 'reference' => null]; $details = $stack->getStackItem('Value', $val, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot);
if ($localeConstant) { if ($localeConstant) {
$details['localeValue'] = $localeConstant; $details['localeValue'] = $localeConstant;
} }
@ -3645,9 +3790,74 @@ class Calculation
$pCellParent = ($pCell !== null) ? $pCell->getParent() : null; $pCellParent = ($pCell !== null) ? $pCell->getParent() : null;
$stack = new Stack(); $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 // Loop through each token in turn
foreach ($tokens as $tokenData) { foreach ($tokens as $tokenData) {
$token = $tokenData['value']; $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 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])) { if (isset(self::$binaryOperators[$token])) {
// We must have two operands, error if we don't // We must have two operands, error if we don't
@ -3677,7 +3887,10 @@ class Calculation
case '<=': // Less than or Equal to case '<=': // Less than or Equal to
case '=': // Equality case '=': // Equality
case '<>': // Inequality case '<>': // Inequality
$this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack); $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
break; break;
// Binary Operators // Binary Operators
@ -3733,23 +3946,38 @@ class Calculation
break; break;
case '+': // Addition case '+': // Addition
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack); $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'plusEquals', $stack);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
break; break;
case '-': // Subtraction case '-': // Subtraction
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack); $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'minusEquals', $stack);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
break; break;
case '*': // Multiplication case '*': // Multiplication
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack); $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayTimesEquals', $stack);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
break; break;
case '/': // Division case '/': // Division
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack); $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'arrayRightDivide', $stack);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
break; break;
case '^': // Exponential case '^': // Exponential
$this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack); $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, 'power', $stack);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
break; break;
case '&': // Concatenation case '&': // Concatenation
@ -3782,6 +4010,10 @@ class Calculation
$this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
$stack->push('Value', $result); $stack->push('Value', $result);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
break; break;
case '|': // Intersect case '|': // Intersect
$rowIntersect = array_intersect_key($operand1, $operand2); $rowIntersect = array_intersect_key($operand1, $operand2);
@ -3826,6 +4058,9 @@ class Calculation
} }
$this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result));
$stack->push('Value', $result); $stack->push('Value', $result);
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
} else { } else {
$this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack); $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack);
} }
@ -3899,9 +4134,23 @@ class Calculation
} }
} }
$stack->push('Value', $cellValue, $cellRef); $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 // 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)) { } 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]; $functionName = $matches[1];
$argCount = $stack->pop(); $argCount = $stack->pop();
$argCount = $argCount['value']; $argCount = $argCount['value'];
@ -3944,6 +4193,7 @@ class Calculation
} }
} }
} }
// Reverse the order of the arguments // Reverse the order of the arguments
krsort($args); krsort($args);
@ -3968,21 +4218,31 @@ class Calculation
} }
unset($arg); unset($arg);
} }
$result = call_user_func_array($functionCall, $args); $result = call_user_func_array($functionCall, $args);
if ($functionName != 'MKMATRIX') { if ($functionName != 'MKMATRIX') {
$this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result)); $this->debugLog->writeDebugLog('Evaluation Result for ', self::localeFunc($functionName), '() function call is ', $this->showTypeDetails($result));
} }
$stack->push('Value', self::wrapResult($result)); $stack->push('Value', self::wrapResult($result));
if (isset($storeKey)) {
$branchStore[$storeKey] = $result;
}
} }
} else { } else {
// if the token is a number, boolean, string or an Excel error, push it onto the stack // if the token is a number, boolean, string or an Excel error, push it onto the stack
if (isset(self::$excelConstants[strtoupper($token)])) { if (isset(self::$excelConstants[strtoupper($token)])) {
$excelConstant = strtoupper($token); $excelConstant = strtoupper($token);
$stack->push('Constant Value', self::$excelConstants[$excelConstant]); $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])); $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] == '#')) { } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == '"') || ($token[0] == '#')) {
$stack->push('Value', $token); $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 // 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)) { } elseif (preg_match('/^' . self::CALCULATION_REGEXP_NAMEDRANGE . '$/i', $token, $matches)) {
$namedRange = $matches[6]; $namedRange = $matches[6];
@ -3992,6 +4252,9 @@ class Calculation
$pCell->attach($pCellParent); $pCell->attach($pCellParent);
$this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue)); $this->debugLog->writeDebugLog('Evaluation Result for named range ', $namedRange, ' is ', $this->showTypeDetails($cellValue));
$stack->push('Named Range', $cellValue, $namedRange); $stack->push('Named Range', $cellValue, $namedRange);
if (isset($storeKey)) {
$branchStore[$storeKey] = $cellValue;
}
} else { } else {
return $this->raiseFormulaError("undefined variable '$token'"); return $this->raiseFormulaError("undefined variable '$token'");
} }
@ -4053,7 +4316,7 @@ class Calculation
* @param Stack $stack * @param Stack $stack
* @param bool $recursingArrays * @param bool $recursingArrays
* *
* @return bool * @return mixed
*/ */
private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false) private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false)
{ {
@ -4090,7 +4353,7 @@ class Calculation
// And push the result onto the stack // And push the result onto the stack
$stack->push('Array', $result); $stack->push('Array', $result);
return true; return $result;
} }
// Simple validate the two operands if they are string values // Simple validate the two operands if they are string values
@ -4180,7 +4443,7 @@ class Calculation
// And push the result onto the stack // And push the result onto the stack
$stack->push('Value', $result); $stack->push('Value', $result);
return true; return $result;
} }
/** /**
@ -4206,7 +4469,7 @@ class Calculation
* @param string $matrixFunction * @param string $matrixFunction
* @param mixed $stack * @param mixed $stack
* *
* @return bool * @return bool|mixed
*/ */
private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack) private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack)
{ {
@ -4284,7 +4547,7 @@ class Calculation
// And push the result onto the stack // And push the result onto the stack
$stack->push('Value', $result); $stack->push('Value', $result);
return true; return $result;
} }
// trigger an error, but nicely, if need be // trigger an error, but nicely, if need be
@ -4488,4 +4751,27 @@ class Calculation
return $args; 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;
}
} }

View File

@ -464,9 +464,10 @@ class LookupRef
* *
* @param mixed $lookupValue The value that you want to match in lookup_array * @param mixed $lookupValue The value that you want to match in lookup_array
* @param mixed $lookupArray The range of cells being searched * @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) public static function MATCH($lookupValue, $lookupArray, $matchType = 1)
{ {
@ -474,9 +475,10 @@ class LookupRef
$lookupValue = Functions::flattenSingleValue($lookupValue); $lookupValue = Functions::flattenSingleValue($lookupValue);
$matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType); $matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType);
$initialLookupValue = $lookupValue; // MATCH is not case sensitive, so we convert lookup value to be lower cased in case it's string type.
// MATCH is not case sensitive if (is_string($lookupValue)) {
$lookupValue = StringHelper::strToLower($lookupValue); $lookupValue = StringHelper::strToLower($lookupValue);
}
// Lookup_value type has to be number, text, or logical values // Lookup_value type has to be number, text, or logical values
if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) { if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
@ -522,16 +524,54 @@ class LookupRef
// find the match // find the match
// ** // **
if ($matchType == 0 || $matchType == 1) { if ($matchType === 0 || $matchType === 1) {
foreach ($lookupArray as $i => $lookupArrayValue) { foreach ($lookupArray as $i => $lookupArrayValue) {
$onlyNumeric = is_numeric($lookupArrayValue) && is_numeric($lookupValue); $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
$onlyNumericExactMatch = $onlyNumeric && $lookupArrayValue == $lookupValue; $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue;
$nonOnlyNumericExactMatch = !$onlyNumeric && $lookupArrayValue === $lookupValue; $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue;
$exactMatch = $onlyNumericExactMatch || $nonOnlyNumericExactMatch; $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch;
if (($matchType == 0) && $exactMatch) {
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 // exact match
return $i + 1; 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); $i = array_search($i, $keySet);
// The current value is the (first) match // The current value is the (first) match
@ -539,26 +579,26 @@ class LookupRef
} }
} }
} else { } 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; $maxValueKey = null;
// The basic algorithm is: // The basic algorithm is:
// Iterate and keep the highest match until the next element is smaller than the searched value. // Iterate and keep the highest match until the next element is smaller than the searched value.
// Return immediately if perfect match is found // Return immediately if perfect match is found
foreach ($lookupArray as $i => $lookupArrayValue) { 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, // Another "special" case. If a perfect match is found,
// the algorithm gives up immediately // the algorithm gives up immediately
return $i + 1; return $i + 1;
} elseif ($lookupArrayValue >= $lookupValue) { } elseif ($typeMatch & $lookupArrayValue >= $lookupValue) {
$maxValueKey = $i + 1; $maxValueKey = $i + 1;
} elseif ($typeMatch & $lookupArrayValue < $lookupValue) {
//Excel algorithm gives up immediately if the first element is smaller than the searched value
break;
} }
} }

View File

@ -36,14 +36,24 @@ class Stack
* @param mixed $type * @param mixed $type
* @param mixed $value * @param mixed $value
* @param mixed $reference * @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) public function push(
{ $type,
$this->stack[$this->count++] = [ $value,
'type' => $type, $reference = null,
'value' => $value, $storeKey = null,
'reference' => $reference, $onlyIf = null,
]; $onlyIfNot = null
) {
$stackItem = $this->getStackItem($type, $value, $reference, $storeKey, $onlyIf, $onlyIfNot);
$this->stack[$this->count++] = $stackItem;
if ($type == 'Function') { if ($type == 'Function') {
$localeFunction = Calculation::localeFunc($value); $localeFunction = Calculation::localeFunc($value);
if ($localeFunction != $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. * Pop the last entry from the stack.
* *
@ -90,4 +129,21 @@ class Stack
$this->stack = []; $this->stack = [];
$this->count = 0; $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;
}
} }

View File

@ -502,10 +502,10 @@ class Html extends BaseReader
if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) { if (isset($attributeArray['rowspan'], $attributeArray['colspan'])) {
//create merging rowspan and colspan //create merging rowspan and colspan
$columnTo = $column; $columnTo = $column;
for ($i = 0; $i < $attributeArray['colspan'] - 1; ++$i) { for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
++$columnTo; ++$columnTo;
} }
$range = $column . $row . ':' . $columnTo . ($row + $attributeArray['rowspan'] - 1); $range = $column . $row . ':' . $columnTo . ($row + (int) $attributeArray['rowspan'] - 1);
foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) { foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
$this->rowspan[$value] = true; $this->rowspan[$value] = true;
} }
@ -513,7 +513,7 @@ class Html extends BaseReader
$column = $columnTo; $column = $columnTo;
} elseif (isset($attributeArray['rowspan'])) { } elseif (isset($attributeArray['rowspan'])) {
//create merging 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) { foreach (Coordinate::extractAllCellReferencesInRange($range) as $value) {
$this->rowspan[$value] = true; $this->rowspan[$value] = true;
} }
@ -521,7 +521,7 @@ class Html extends BaseReader
} elseif (isset($attributeArray['colspan'])) { } elseif (isset($attributeArray['colspan'])) {
//create merging colspan //create merging colspan
$columnTo = $column; $columnTo = $column;
for ($i = 0; $i < $attributeArray['colspan'] - 1; ++$i) { for ($i = 0; $i < (int) $attributeArray['colspan'] - 1; ++$i) {
++$columnTo; ++$columnTo;
} }
$sheet->mergeCells($column . $row . ':' . $columnTo . $row); $sheet->mergeCells($column . $row . ':' . $columnTo . $row);
@ -592,12 +592,6 @@ class Html extends BaseReader
throw new Exception($pFilename . ' is an Invalid HTML file.'); 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 // Create a new DOM object
$dom = new DOMDocument(); $dom = new DOMDocument();
// Reload the HTML file into the DOM object // 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'); 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 // Discard white space
$dom->preserveWhiteSpace = false; $document->preserveWhiteSpace = false;
$row = 0; $row = 0;
$column = 'A'; $column = 'A';
$content = ''; $content = '';
$this->rowspan = []; $this->rowspan = [];
$this->processDomElement($dom, $spreadsheet->getActiveSheet(), $row, $column, $content); $this->processDomElement($document, $spreadsheet->getActiveSheet(), $row, $column, $content);
// Return // Return
return $spreadsheet; return $spreadsheet;

View File

@ -23,8 +23,8 @@ class NumberFormat extends Supervisor
const FORMAT_PERCENTAGE_00 = '0.00%'; const FORMAT_PERCENTAGE_00 = '0.00%';
const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'; const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd';
const FORMAT_DATE_YYYYMMDD = 'yy-mm-dd'; const FORMAT_DATE_YYYYMMDD = 'yyyy-mm-dd';
const FORMAT_DATE_DDMMYYYY = 'dd/mm/yy'; const FORMAT_DATE_DDMMYYYY = 'dd/mm/yyyy';
const FORMAT_DATE_DMYSLASH = 'd/m/yy'; const FORMAT_DATE_DMYSLASH = 'd/m/yy';
const FORMAT_DATE_DMYMINUS = 'd-m-yy'; const FORMAT_DATE_DMYMINUS = 'd-m-yy';
const FORMAT_DATE_DMMINUS = 'd-m'; const FORMAT_DATE_DMMINUS = 'd-m';
@ -43,7 +43,7 @@ class NumberFormat extends Supervisor
const FORMAT_DATE_TIME6 = 'h:mm:ss'; const FORMAT_DATE_TIME6 = 'h:mm:ss';
const FORMAT_DATE_TIME7 = 'i:s.S'; const FORMAT_DATE_TIME7 = 'i:s.S';
const FORMAT_DATE_TIME8 = 'h:mm:ss;@'; 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_SIMPLE = '"$"#,##0.00_-';
const FORMAT_CURRENCY_USD = '$#,##0_-'; const FORMAT_CURRENCY_USD = '$#,##0_-';

View File

@ -1441,7 +1441,7 @@ class Worksheet implements IComparable
$this->parent->setActiveSheetIndex($this->parent->getIndex($this)); $this->parent->setActiveSheetIndex($this->parent->getIndex($this));
// set cell coordinate as active // set cell coordinate as active
$this->setSelectedCells(strtoupper($pCellCoordinate)); $this->setSelectedCells($pCellCoordinate);
return $this->parent->getCellXfSupervisor(); return $this->parent->getCellXfSupervisor();
} }
@ -2115,6 +2115,10 @@ class Worksheet implements IComparable
public function removeRow($pRow, $pNumRows = 1) public function removeRow($pRow, $pNumRows = 1)
{ {
if ($pRow >= 1) { if ($pRow >= 1) {
for ($r = 0; $r < $pNumRows; ++$r) {
$this->getCellCollection()->removeRow($pRow + $r);
}
$highestRow = $this->getHighestDataRow(); $highestRow = $this->getHighestDataRow();
$objReferenceHelper = ReferenceHelper::getInstance(); $objReferenceHelper = ReferenceHelper::getInstance();
$objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this); $objReferenceHelper->insertNewBefore('A' . ($pRow + $pNumRows), 0, -$pNumRows, $this);

View File

@ -891,8 +891,8 @@ class Html extends BaseWriter
$css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt'; $css['table.sheet' . $sheetIndex . ' col.col' . $column]['width'] = $width . 'pt';
if ($columnDimension->getVisible() === false) { if ($columnDimension->getVisible() === false) {
$css['table.sheet' . $sheetIndex . ' col.col' . $column]['visibility'] = 'collapse'; $css['table.sheet' . $sheetIndex . ' .column' . $column]['visibility'] = 'collapse';
$css['table.sheet' . $sheetIndex . ' col.col' . $column]['*display'] = 'none'; // target IE6+7 $css['table.sheet' . $sheetIndex . ' .column' . $column]['display'] = 'none'; // target IE6+7
} }
} }
} }

View File

@ -163,4 +163,195 @@ class CalculationTest extends TestCase
self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue()); 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';
}
} }

View File

@ -299,6 +299,36 @@ class HtmlTest extends TestCase
unlink($filename); 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 * @param string $html
* *
@ -321,4 +351,14 @@ class HtmlTest extends TestCase
{ {
return (new Html())->load($filename); 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);
}
} }

View File

@ -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));
}
}

View File

@ -138,6 +138,10 @@ class WorksheetTest extends TestCase
['testTitle!B2', 'testTitle', 'B2', 'B2'], ['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'], ['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($expectTitle, $arRange[0]);
self::assertSame($expectCell2, $arRange[1]); 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()
);
}
} }

View File

@ -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();

View File

@ -102,7 +102,147 @@ return [
3, 3,
'x', 'x',
[[0], [0], ['x'], ['x'], ['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,
], ],
]; ];

View File

@ -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>