diff --git a/CHANGELOG.md b/CHANGELOG.md index 26b1d35b..1aed31e4 100644 --- a/CHANGELOG.md +++ b/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 @@ -48,10 +70,10 @@ and this project adheres to [Semantic Versioning](https://semver.org). `XmlScanner::threadSafeLibxmlDisableEntityLoaderAvailability()` - Provide an option to disable the libxml_disable_entity_loader call through settings. This is not recommended as it reduces the security of the XML-based readers, and should only be used if you understand the consequences and have no other choice. - + ### 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 @@ -71,7 +93,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix handling for escaped enclosures and new lines in CSV Separator Inference - Fix MATCH an error was appearing when comparing strings against 0 (always true) - Fix wrong calculation of highest column with specified row [#700](https://github.com/PHPOffice/PhpSpreadsheet/issues/700) -- Fix VLOOKUP +- Fix VLOOKUP - Fix return type hint ## [1.6.0] - 2019-01-02 @@ -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 @@ -202,15 +224,15 @@ and this project adheres to [Semantic Versioning](https://semver.org). - IMSEC() Returns the secant of a complex number - IMSECH() Returns the hyperbolic secant of a complex number - IMSINH() Returns the hyperbolic sine of a complex number - - IMTAN() Returns the tangent of a complex number + - IMTAN() Returns the tangent of a complex number ### 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 diff --git a/docs/topics/reading-and-writing-to-file.md b/docs/topics/reading-and-writing-to-file.md index 0b27f8c1..66455356 100644 --- a/docs/topics/reading-and-writing-to-file.md +++ b/docs/topics/reading-and-writing-to-file.md @@ -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 = ' + + + + + + + + + +
Hello World
Hello
World
Hello
World
'; + +$reader = new \PhpOffice\PhpSpreadsheet\Reader\Html(); +$spreadsheet = $reader->loadFromString($htmlString); + +$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, 'Xls'); +$writer->save('write.xls'); +``` diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 63cd69cc..f2e6647f 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -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 + // 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,22 +4218,32 @@ 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 the token is a named range, push the named range name onto the stack + 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]; $this->debugLog->writeDebugLog('Evaluating Named Range ', $namedRange); @@ -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; + } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 5bc0a435..48434300 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -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 - $lookupValue = StringHelper::strToLower($lookupValue); + // 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) { - // exact match - return $i + 1; - } elseif (($matchType == 1) && ($lookupArrayValue <= $lookupValue)) { + $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 ($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; } } diff --git a/src/PhpSpreadsheet/Calculation/Token/Stack.php b/src/PhpSpreadsheet/Calculation/Token/Stack.php index b8dccf95..341a0179 100644 --- a/src/PhpSpreadsheet/Calculation/Token/Stack.php +++ b/src/PhpSpreadsheet/Calculation/Token/Stack.php @@ -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; + } } diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index d599424d..bf9c6038 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -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,28 +592,64 @@ 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 + // Create a new DOM object $dom = new DOMDocument(); - // Reload the HTML file into the DOM object + // Reload the HTML file into the DOM object $loaded = $dom->loadHTML(mb_convert_encoding($this->securityScanner->scanFile($pFilename), 'HTML-ENTITIES', 'UTF-8')); if ($loaded === false) { throw new Exception('Failed to load ' . $pFilename . ' as a DOM Document'); } - // Discard white space - $dom->preserveWhiteSpace = false; + 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 + $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; diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index ac64b20e..6577aeff 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -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_-'; diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 689c547e..7da5f693 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -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); diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index ebda5c33..d626e9c4 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -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 } } } diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php index 4b81fbf4..4cdfe5cb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php @@ -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'; + } } diff --git a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php b/tests/PhpSpreadsheetTests/Reader/HtmlTest.php index 415f562a..e9dd207f 100644 --- a/tests/PhpSpreadsheetTests/Reader/HtmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/HtmlTest.php @@ -33,7 +33,7 @@ class HtmlTest extends TestCase /** * @dataProvider providerCanReadVerySmallFile * - * @param bool $expected + * @param bool $expected * @param string $content */ public function testCanReadVerySmallFile($expected, $content) @@ -299,6 +299,36 @@ class HtmlTest extends TestCase unlink($filename); } + public function testCanLoadFromString() + { + $html = ' + + + + + + + + + +
Hello World
Hello
World
Hello
World
'; + $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); + } } diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php new file mode 100644 index 00000000..5173bf22 --- /dev/null +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -0,0 +1,56 @@ +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)); + } +} diff --git a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php index ce2df837..eb2aa200 100644 --- a/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php +++ b/tests/PhpSpreadsheetTests/Worksheet/WorksheetTest.php @@ -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() + ); + } } diff --git a/tests/data/Calculation/Calculation.php b/tests/data/Calculation/Calculation.php new file mode 100644 index 00000000..201df47a --- /dev/null +++ b/tests/data/Calculation/Calculation.php @@ -0,0 +1,61 @@ +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(); diff --git a/tests/data/Calculation/LookupRef/MATCH.php b/tests/data/Calculation/LookupRef/MATCH.php index 84644949..b39edb9f 100644 --- a/tests/data/Calculation/LookupRef/MATCH.php +++ b/tests/data/Calculation/LookupRef/MATCH.php @@ -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, ], - ]; diff --git a/tests/data/Reader/HTML/rowspan.html b/tests/data/Reader/HTML/rowspan.html new file mode 100644 index 00000000..c748c8e3 --- /dev/null +++ b/tests/data/Reader/HTML/rowspan.html @@ -0,0 +1,14 @@ + + + + + + + + + + + + +
A1B1C1D1
A2 with invalid colspanD2 +