From efc63be0393bdc21cb369a76a55d1ffeb5cc4370 Mon Sep 17 00:00:00 2001 From: David Woloszyn Date: Fri, 7 Oct 2022 17:01:12 +1100 Subject: [PATCH] MDL-75484 lib: PHPSpreadSheet upgraded to 1.25.2 --- lib/phpspreadsheet/vendor/autoload.php | 7 +- .../vendor/composer/InstalledVersions.php | 16 +- .../vendor/composer/autoload_classmap.php | 2 +- .../vendor/composer/autoload_namespaces.php | 2 +- .../vendor/composer/autoload_psr4.php | 2 +- .../vendor/composer/autoload_real.php | 31 +- .../vendor/composer/autoload_static.php | 8 +- .../vendor/composer/installed.json | 35 +- .../vendor/composer/installed.php | 24 +- .../phpoffice/phpspreadsheet/CHANGELOG.md | 322 ++++ .../vendor/phpoffice/phpspreadsheet/README.md | 74 + .../phpoffice/phpspreadsheet/composer.json | 26 +- .../Calculation/ArrayEnabled.php | 133 ++ .../Calculation/BinaryComparison.php | 181 ++ .../Calculation/Calculation.php | 966 ++++++----- .../Calculation/Database/DGet.php | 4 +- .../PhpSpreadsheet/Calculation/DateTime.php | 38 +- .../Calculation/DateTimeExcel/Current.php | 6 +- .../Calculation/DateTimeExcel/Date.php | 34 +- .../Calculation/DateTimeExcel/DateParts.php | 30 +- .../Calculation/DateTimeExcel/DateValue.php | 28 +- .../Calculation/DateTimeExcel/Days.php | 25 +- .../Calculation/DateTimeExcel/Days360.php | 24 +- .../Calculation/DateTimeExcel/Difference.php | 24 +- .../Calculation/DateTimeExcel/Helpers.php | 13 +- .../Calculation/DateTimeExcel/Month.php | 27 +- .../Calculation/DateTimeExcel/NetworkDays.php | 21 +- .../Calculation/DateTimeExcel/Time.php | 25 +- .../Calculation/DateTimeExcel/TimeParts.php | 34 +- .../Calculation/DateTimeExcel/TimeValue.php | 17 +- .../Calculation/DateTimeExcel/Week.php | 52 +- .../Calculation/DateTimeExcel/WorkDay.php | 42 +- .../Calculation/DateTimeExcel/YearFrac.php | 29 +- .../Engine/ArrayArgumentHelper.php | 209 +++ .../Engine/ArrayArgumentProcessor.php | 175 ++ .../Calculation/Engine/BranchPruner.php | 223 +++ .../Calculation/Engine/Logger.php | 12 +- .../Calculation/Engineering.php | 122 +- .../Calculation/Engineering/BesselI.php | 20 +- .../Calculation/Engineering/BesselJ.php | 20 +- .../Calculation/Engineering/BesselK.php | 40 +- .../Calculation/Engineering/BesselY.php | 39 +- .../Calculation/Engineering/BitWise.php | 102 +- .../Calculation/Engineering/Compare.php | 42 +- .../Calculation/Engineering/Complex.php | 50 +- .../Engineering/ComplexFunctions.php | 270 ++- .../Engineering/ComplexOperations.php | 42 +- .../Calculation/Engineering/ConvertBase.php | 14 +- .../Calculation/Engineering/ConvertBinary.php | 59 +- .../Engineering/ConvertDecimal.php | 70 +- .../Calculation/Engineering/ConvertHex.php | 61 +- .../Calculation/Engineering/ConvertOctal.php | 61 +- .../Calculation/Engineering/ConvertUOM.php | 32 +- .../Engineering/EngineeringValidations.php | 6 +- .../Calculation/Engineering/Erf.php | 26 +- .../Calculation/Engineering/ErfC.php | 17 +- .../Calculation/ExceptionHandler.php | 4 +- .../PhpSpreadsheet/Calculation/Financial.php | 12 +- .../Calculation/Financial/Amortization.php | 18 +- .../CashFlow/CashFlowValidations.php | 4 +- .../Financial/CashFlow/Constant/Periodic.php | 7 +- .../CashFlow/Constant/Periodic/Cumulative.php | 5 +- .../CashFlow/Constant/Periodic/Interest.php | 11 +- .../CashFlow/Constant/Periodic/Payments.php | 3 +- .../Calculation/Financial/CashFlow/Single.php | 5 +- .../CashFlow/Variable/NonPeriodic.php | 57 +- .../Financial/CashFlow/Variable/Periodic.php | 13 +- .../Calculation/Financial/Coupons.php | 10 +- .../Calculation/Financial/Depreciation.php | 19 +- .../Calculation/Financial/Dollar.php | 73 +- .../Financial/FinancialValidations.php | 22 +- .../Calculation/Financial/Helpers.php | 6 +- .../Calculation/Financial/InterestRate.php | 5 +- .../Financial/Securities/AccruedInterest.php | 6 +- .../Financial/Securities/Price.php | 13 +- .../Financial/Securities/Rates.php | 9 +- .../Securities/SecurityValidations.php | 6 +- .../Financial/Securities/Yields.php | 8 +- .../Calculation/Financial/TreasuryBill.php | 19 +- .../Calculation/FormulaParser.php | 2 +- .../PhpSpreadsheet/Calculation/Functions.php | 563 ++++--- .../Calculation/Information/ErrorValue.php | 71 + .../Calculation/Information/ExcelError.php | 171 ++ .../Calculation/Information/Value.php | 328 ++++ .../PhpSpreadsheet/Calculation/Logical.php | 2 +- .../Calculation/Logical/Conditional.php | 58 +- .../Calculation/Logical/Operations.php | 23 +- .../PhpSpreadsheet/Calculation/LookupRef.php | 6 +- .../Calculation/LookupRef/Address.php | 44 +- .../Calculation/LookupRef/ExcelMatch.php | 25 +- .../Calculation/LookupRef/Filter.php | 81 + .../Calculation/LookupRef/Formula.php | 6 +- .../Calculation/LookupRef/HLookup.php | 31 +- .../Calculation/LookupRef/Helpers.php | 12 +- .../Calculation/LookupRef/Hyperlink.php | 3 +- .../Calculation/LookupRef/Indirect.php | 44 +- .../Calculation/LookupRef/Lookup.php | 11 +- .../Calculation/LookupRef/LookupBase.php | 32 +- .../LookupRef/LookupRefValidations.php | 9 +- .../Calculation/LookupRef/Matrix.php | 38 +- .../Calculation/LookupRef/Offset.php | 20 +- .../LookupRef/RowColumnInformation.php | 18 +- .../Calculation/LookupRef/Selection.php | 19 +- .../Calculation/LookupRef/Sort.php | 342 ++++ .../Calculation/LookupRef/Unique.php | 141 ++ .../Calculation/LookupRef/VLookup.php | 40 +- .../PhpSpreadsheet/Calculation/MathTrig.php | 196 +-- .../Calculation/MathTrig/Absolute.php | 13 +- .../Calculation/MathTrig/Angle.php | 23 +- .../Calculation/MathTrig/Arabic.php | 19 +- .../Calculation/MathTrig/Base.php | 29 +- .../Calculation/MathTrig/Ceiling.php | 45 +- .../Calculation/MathTrig/Combinations.php | 35 +- .../Calculation/MathTrig/Exp.php | 13 +- .../Calculation/MathTrig/Factorial.php | 23 +- .../Calculation/MathTrig/Floor.php | 47 +- .../Calculation/MathTrig/Gcd.php | 3 +- .../Calculation/MathTrig/Helpers.php | 15 +- .../Calculation/MathTrig/IntClass.php | 13 +- .../Calculation/MathTrig/Lcm.php | 3 +- .../Calculation/MathTrig/Logarithms.php | 31 +- .../Calculation/MathTrig/MatrixFunctions.php | 57 +- .../Calculation/MathTrig/Operations.php | 68 +- .../Calculation/MathTrig/Random.php | 64 +- .../Calculation/MathTrig/Roman.php | 19 +- .../Calculation/MathTrig/Round.php | 75 +- .../Calculation/MathTrig/SeriesSum.php | 9 +- .../Calculation/MathTrig/Sign.php | 13 +- .../Calculation/MathTrig/Sqrt.php | 23 +- .../Calculation/MathTrig/Subtotal.php | 58 +- .../Calculation/MathTrig/Sum.php | 11 +- .../Calculation/MathTrig/SumSquares.php | 3 +- .../Calculation/MathTrig/Trig/Cosecant.php | 23 +- .../Calculation/MathTrig/Trig/Cosine.php | 43 +- .../Calculation/MathTrig/Trig/Cotangent.php | 43 +- .../Calculation/MathTrig/Trig/Secant.php | 23 +- .../Calculation/MathTrig/Trig/Sine.php | 43 +- .../Calculation/MathTrig/Trig/Tangent.php | 60 +- .../Calculation/MathTrig/Trunc.php | 17 +- .../Calculation/Statistical.php | 68 +- .../Calculation/Statistical/Averages.php | 29 +- .../Calculation/Statistical/Averages/Mean.php | 13 +- .../Calculation/Statistical/Confidence.php | 23 +- .../Calculation/Statistical/Deviations.php | 17 +- .../Statistical/Distributions/Beta.php | 50 +- .../Statistical/Distributions/Binomial.php | 93 +- .../Statistical/Distributions/ChiSquared.php | 74 +- .../Distributions/DistributionValidations.php | 4 +- .../Statistical/Distributions/Exponential.php | 20 +- .../Statistical/Distributions/F.php | 22 +- .../Statistical/Distributions/Fisher.php | 25 +- .../Statistical/Distributions/Gamma.php | 58 +- .../Statistical/Distributions/GammaBase.php | 3 +- .../Distributions/HyperGeometric.php | 35 +- .../Statistical/Distributions/LogNormal.php | 56 +- .../Distributions/NewtonRaphson.php | 3 +- .../Statistical/Distributions/Normal.php | 36 +- .../Statistical/Distributions/Poisson.php | 27 +- .../Distributions/StandardNormal.php | 73 +- .../Statistical/Distributions/StudentT.php | 32 +- .../Statistical/Distributions/Weibull.php | 22 +- .../Calculation/Statistical/Percentiles.php | 13 +- .../Calculation/Statistical/Permutations.php | 31 +- .../Calculation/Statistical/Size.php | 13 +- .../Calculation/Statistical/Standardize.php | 26 +- .../Statistical/StatisticalValidations.php | 8 +- .../Calculation/Statistical/Trends.php | 25 +- .../Calculation/Statistical/Variances.php | 13 +- .../PhpSpreadsheet/Calculation/TextData.php | 90 +- .../Calculation/TextData/CaseConvert.php | 40 +- .../Calculation/TextData/CharacterConvert.php | 28 +- .../Calculation/TextData/Concatenate.php | 82 +- .../Calculation/TextData/Extract.php | 222 ++- .../Calculation/TextData/Format.php | 120 +- .../Calculation/TextData/Helpers.php | 17 +- .../Calculation/TextData/Replace.php | 84 +- .../Calculation/TextData/Search.php | 31 +- .../Calculation/TextData/Text.php | 208 ++- .../Calculation/TextData/Trim.php | 24 +- .../Calculation/Token/Stack.php | 92 +- .../Calculation/Web/Service.php | 14 +- .../Calculation/functionlist.txt | 400 ----- .../Calculation/locale/Translations.xlsx | Bin 109827 -> 111436 bytes .../Calculation/locale/da/functions | 1 + .../Calculation/locale/de/functions | 1 + .../Calculation/locale/es/functions | 1 + .../Calculation/locale/fi/functions | 1 + .../Calculation/locale/fr/functions | 1 + .../Calculation/locale/hu/functions | 1 + .../Calculation/locale/nb/functions | 1 + .../Calculation/locale/nl/functions | 1 + .../Calculation/locale/pt/br/functions | 1 + .../Calculation/locale/pt/functions | 1 + .../Calculation/locale/ru/functions | 21 +- .../Calculation/locale/sv/functions | 1 + .../src/PhpSpreadsheet/Cell/AddressHelper.php | 37 +- .../src/PhpSpreadsheet/Cell/AddressRange.php | 22 + .../Cell/AdvancedValueBinder.php | 5 + .../src/PhpSpreadsheet/Cell/Cell.php | 153 +- .../src/PhpSpreadsheet/Cell/CellAddress.php | 166 ++ .../src/PhpSpreadsheet/Cell/CellRange.php | 136 ++ .../src/PhpSpreadsheet/Cell/ColumnRange.php | 125 ++ .../src/PhpSpreadsheet/Cell/Coordinate.php | 119 +- .../src/PhpSpreadsheet/Cell/DataType.php | 12 +- .../src/PhpSpreadsheet/Cell/DataValidator.php | 7 +- .../src/PhpSpreadsheet/Cell/RowRange.php | 93 ++ .../PhpSpreadsheet/CellReferenceHelper.php | 119 ++ .../src/PhpSpreadsheet/Chart/Axis.php | 565 ++----- .../src/PhpSpreadsheet/Chart/Chart.php | 346 ++-- .../src/PhpSpreadsheet/Chart/ChartColor.php | 177 ++ .../src/PhpSpreadsheet/Chart/DataSeries.php | 39 +- .../PhpSpreadsheet/Chart/DataSeriesValues.php | 256 ++- .../src/PhpSpreadsheet/Chart/GridLines.php | 441 ----- .../src/PhpSpreadsheet/Chart/Layout.php | 238 ++- .../src/PhpSpreadsheet/Chart/Legend.php | 16 +- .../src/PhpSpreadsheet/Chart/PlotArea.php | 71 +- .../src/PhpSpreadsheet/Chart/Properties.php | 1072 +++++++++--- .../PhpSpreadsheet/Chart/Renderer/JpGraph.php | 862 +--------- .../Chart/Renderer/JpGraphRendererBase.php | 852 ++++++++++ .../Chart/Renderer/MtJpGraphRenderer.php | 36 + .../Chart/Renderer/PHP Charting Libraries.txt | 7 +- .../src/PhpSpreadsheet/Chart/Title.php | 9 +- .../src/PhpSpreadsheet/Chart/TrendLine.php | 226 +++ .../src/PhpSpreadsheet/Collection/Cells.php | 229 ++- .../Collection/CellsFactory.php | 3 +- .../{Memory.php => Memory/SimpleCache1.php} | 7 +- .../Collection/Memory/SimpleCache3.php | 109 ++ .../src/PhpSpreadsheet/DefinedName.php | 2 +- .../PhpSpreadsheet/Document/Properties.php | 8 +- .../src/PhpSpreadsheet/Helper/Dimension.php | 16 +- .../src/PhpSpreadsheet/Helper/Html.php | 8 +- .../src/PhpSpreadsheet/Helper/Sample.php | 32 +- .../src/PhpSpreadsheet/Helper/TextGrid.php | 139 ++ .../src/PhpSpreadsheet/IOFactory.php | 88 +- .../src/PhpSpreadsheet/Reader/BaseReader.php | 38 +- .../src/PhpSpreadsheet/Reader/Csv.php | 152 +- .../PhpSpreadsheet/Reader/Csv/Delimiter.php | 6 +- .../src/PhpSpreadsheet/Reader/Gnumeric.php | 141 +- .../Reader/Gnumeric/PageSetup.php | 5 +- .../src/PhpSpreadsheet/Reader/Html.php | 44 +- .../src/PhpSpreadsheet/Reader/Ods.php | 161 +- .../PhpSpreadsheet/Reader/Ods/AutoFilter.php | 4 +- .../PhpSpreadsheet/Reader/Ods/BaseLoader.php | 27 + .../PhpSpreadsheet/Reader/Ods/BaseReader.php | 77 - .../Reader/Ods/DefinedNames.php | 10 +- .../Reader/Ods/FormulaTranslator.php | 96 ++ .../Reader/Ods/PageSettings.php | 47 + .../PhpSpreadsheet/Reader/Ods/Properties.php | 3 +- .../Reader/Security/XmlScanner.php | 2 +- .../src/PhpSpreadsheet/Reader/Slk.php | 8 +- .../src/PhpSpreadsheet/Reader/Xlsx.php | 390 +++-- .../PhpSpreadsheet/Reader/Xlsx/AutoFilter.php | 28 +- .../Reader/Xlsx/BaseParserClass.php | 5 +- .../src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 1080 ++++++++++-- .../Reader/Xlsx/ColumnAndRowAttributes.php | 12 +- .../Reader/Xlsx/ConditionalStyles.php | 159 +- .../Reader/Xlsx/DataValidations.php | 4 +- .../PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php | 2 + .../PhpSpreadsheet/Reader/Xlsx/Namespaces.php | 6 + .../PhpSpreadsheet/Reader/Xlsx/PageSetup.php | 17 +- .../PhpSpreadsheet/Reader/Xlsx/Properties.php | 2 +- .../Reader/Xlsx/SheetViewOptions.php | 11 +- .../src/PhpSpreadsheet/Reader/Xlsx/Styles.php | 248 ++- .../src/PhpSpreadsheet/Reader/Xlsx/Theme.php | 29 +- .../Reader/Xlsx/WorkbookView.php | 153 ++ .../src/PhpSpreadsheet/Reader/Xml.php | 13 +- .../PhpSpreadsheet/Reader/Xml/Properties.php | 2 +- .../src/PhpSpreadsheet/ReferenceHelper.php | 762 +++++---- .../src/PhpSpreadsheet/RichText/RichText.php | 11 +- .../PhpSpreadsheet/RichText/TextElement.php | 15 - .../src/PhpSpreadsheet/Settings.php | 10 +- .../src/PhpSpreadsheet/Shared/Date.php | 39 +- .../src/PhpSpreadsheet/Shared/Drawing.php | 10 + .../src/PhpSpreadsheet/Shared/File.php | 6 - .../src/PhpSpreadsheet/Shared/Font.php | 569 +++---- .../Shared/JAMA/EigenvalueDecomposition.php | 10 +- .../src/PhpSpreadsheet/Shared/JAMA/Matrix.php | 92 +- .../JAMA/SingularValueDecomposition.php | 6 +- .../PhpSpreadsheet/Shared/StringHelper.php | 147 +- .../src/PhpSpreadsheet/Shared/TimeZone.php | 10 +- .../Shared/Trend/PolynomialBestFit.php | 1 + .../src/PhpSpreadsheet/Shared/Trend/Trend.php | 5 +- .../src/PhpSpreadsheet/Shared/XMLWriter.php | 2 + .../src/PhpSpreadsheet/Spreadsheet.php | 75 +- .../src/PhpSpreadsheet/Style/Alignment.php | 69 +- .../src/PhpSpreadsheet/Style/Color.php | 203 +-- .../src/PhpSpreadsheet/Style/Conditional.php | 41 +- .../ConditionalFormatting/CellMatcher.php | 312 ++++ .../CellStyleAssessor.php | 45 + .../ConditionalDataBar.php | 21 +- .../ConditionalFormatting/StyleMerger.php | 118 ++ .../Style/ConditionalFormatting/Wizard.php | 95 ++ .../ConditionalFormatting/Wizard/Blanks.php | 99 ++ .../Wizard/CellValue.php | 189 +++ .../Wizard/DateValue.php | 111 ++ .../Wizard/Duplicates.php | 78 + .../ConditionalFormatting/Wizard/Errors.php | 95 ++ .../Wizard/Expression.php | 73 + .../Wizard/TextValue.php | 163 ++ .../Wizard/WizardAbstract.php | 197 +++ .../Wizard/WizardInterface.php | 25 + .../src/PhpSpreadsheet/Style/Font.php | 251 ++- .../src/PhpSpreadsheet/Style/NumberFormat.php | 2 + .../Style/NumberFormat/DateFormatter.php | 113 +- .../Style/NumberFormat/Formatter.php | 13 +- .../Style/NumberFormat/FractionFormatter.php | 5 +- .../NumberFormat/PercentageFormatter.php | 19 +- .../src/PhpSpreadsheet/Style/Style.php | 6 +- .../PhpSpreadsheet/Worksheet/AutoFilter.php | 30 +- .../Worksheet/AutoFilter/Column.php | 2 +- .../PhpSpreadsheet/Worksheet/BaseDrawing.php | 360 ++-- .../PhpSpreadsheet/Worksheet/CellIterator.php | 15 +- .../src/PhpSpreadsheet/Worksheet/Column.php | 57 +- .../Worksheet/ColumnCellIterator.php | 33 +- .../Worksheet/ColumnDimension.php | 38 +- .../PhpSpreadsheet/Worksheet/Dimension.php | 15 - .../src/PhpSpreadsheet/Worksheet/Drawing.php | 2 +- .../Worksheet/MemoryDrawing.php | 17 +- .../PhpSpreadsheet/Worksheet/PageSetup.php | 14 +- .../src/PhpSpreadsheet/Worksheet/Row.php | 44 +- .../Worksheet/RowCellIterator.php | 26 +- .../PhpSpreadsheet/Worksheet/RowDimension.php | 9 +- .../src/PhpSpreadsheet/Worksheet/Table.php | 454 +++++ .../PhpSpreadsheet/Worksheet/Table/Column.php | 203 +++ .../Worksheet/Table/TableStyle.php | 230 +++ .../PhpSpreadsheet/Worksheet/Validations.php | 101 ++ .../PhpSpreadsheet/Worksheet/Worksheet.php | 889 +++++++--- .../src/PhpSpreadsheet/Writer/BaseWriter.php | 4 +- .../src/PhpSpreadsheet/Writer/Html.php | 265 +-- .../src/PhpSpreadsheet/Writer/IWriter.php | 2 + .../src/PhpSpreadsheet/Writer/Ods.php | 3 +- .../PhpSpreadsheet/Writer/Ods/Cell/Style.php | 84 + .../src/PhpSpreadsheet/Writer/Ods/Content.php | 79 +- .../PhpSpreadsheet/Writer/Ods/Settings.php | 108 +- .../src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 13 +- .../src/PhpSpreadsheet/Writer/Pdf/Mpdf.php | 3 + .../src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php | 2 +- .../src/PhpSpreadsheet/Writer/Xlsx.php | 33 +- .../src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 1453 ++++++++++------- .../PhpSpreadsheet/Writer/Xlsx/Comments.php | 6 +- .../Writer/Xlsx/ContentTypes.php | 10 + .../Writer/Xlsx/DefinedNames.php | 2 +- .../PhpSpreadsheet/Writer/Xlsx/DocProps.php | 6 +- .../PhpSpreadsheet/Writer/Xlsx/Drawing.php | 146 +- .../src/PhpSpreadsheet/Writer/Xlsx/Rels.php | 21 +- .../Writer/Xlsx/StringTable.php | 164 +- .../src/PhpSpreadsheet/Writer/Xlsx/Style.php | 135 +- .../src/PhpSpreadsheet/Writer/Xlsx/Table.php | 111 ++ .../PhpSpreadsheet/Writer/Xlsx/Workbook.php | 14 +- .../PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 386 +++-- .../src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php | 5 +- 351 files changed, 20962 insertions(+), 8620 deletions(-) create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ArrayEnabled.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/BranchPruner.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/ExcelError.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php delete mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/functionlist.txt create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellAddress.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellRange.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/ColumnRange.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/RowRange.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/CellReferenceHelper.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/TrendLine.php rename lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/{Memory.php => Memory/SimpleCache1.php} (93%) create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory/SimpleCache3.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseLoader.php delete mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseReader.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/StyleMerger.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardInterface.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/Column.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php create mode 100644 lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Table.php diff --git a/lib/phpspreadsheet/vendor/autoload.php b/lib/phpspreadsheet/vendor/autoload.php index afaa78f0390..4a49a27d601 100644 --- a/lib/phpspreadsheet/vendor/autoload.php +++ b/lib/phpspreadsheet/vendor/autoload.php @@ -2,6 +2,11 @@ // autoload.php @generated by Composer +if (PHP_VERSION_ID < 50600) { + echo 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL; + exit(1); +} + require_once __DIR__ . '/composer/autoload_real.php'; -return ComposerAutoloaderInit47a82a2b792e78d18b5f54d474d822dc::getLoader(); +return ComposerAutoloaderInite0d802f9161d0d84fba97a5520240273::getLoader(); diff --git a/lib/phpspreadsheet/vendor/composer/InstalledVersions.php b/lib/phpspreadsheet/vendor/composer/InstalledVersions.php index d50e0c9fcc4..c6b54af7ba2 100644 --- a/lib/phpspreadsheet/vendor/composer/InstalledVersions.php +++ b/lib/phpspreadsheet/vendor/composer/InstalledVersions.php @@ -21,12 +21,14 @@ use Composer\Semver\VersionParser; * See also https://getcomposer.org/doc/07-runtime.md#installed-versions * * To require its presence, you can require `composer-runtime-api ^2.0` + * + * @final */ class InstalledVersions { /** * @var mixed[]|null - * @psalm-var array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array}|array{}|null + * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array}|array{}|null */ private static $installed; @@ -37,7 +39,7 @@ class InstalledVersions /** * @var array[] - * @psalm-var array}> + * @psalm-var array}> */ private static $installedByVendor = array(); @@ -241,7 +243,7 @@ class InstalledVersions /** * @return array - * @psalm-return array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string} + * @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool} */ public static function getRootPackage() { @@ -255,7 +257,7 @@ class InstalledVersions * * @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect. * @return array[] - * @psalm-return array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} + * @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} */ public static function getRawData() { @@ -278,7 +280,7 @@ class InstalledVersions * Returns the raw data of all installed.php which are currently loaded for custom implementations * * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ public static function getAllRawData() { @@ -301,7 +303,7 @@ class InstalledVersions * @param array[] $data A vendor/composer/installed.php data set * @return void * - * @psalm-param array{root: array{name: string, version: string, reference: string, pretty_version: string, aliases: string[], dev: bool, install_path: string, type: string}, versions: array} $data + * @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array} $data */ public static function reload($data) { @@ -311,7 +313,7 @@ class InstalledVersions /** * @return array[] - * @psalm-return list}> + * @psalm-return list}> */ private static function getInstalled() { diff --git a/lib/phpspreadsheet/vendor/composer/autoload_classmap.php b/lib/phpspreadsheet/vendor/composer/autoload_classmap.php index b26f1b13b1f..0fb0a2c194b 100644 --- a/lib/phpspreadsheet/vendor/composer/autoload_classmap.php +++ b/lib/phpspreadsheet/vendor/composer/autoload_classmap.php @@ -2,7 +2,7 @@ // autoload_classmap.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( diff --git a/lib/phpspreadsheet/vendor/composer/autoload_namespaces.php b/lib/phpspreadsheet/vendor/composer/autoload_namespaces.php index b7fc0125dbc..15a2ff3ad6d 100644 --- a/lib/phpspreadsheet/vendor/composer/autoload_namespaces.php +++ b/lib/phpspreadsheet/vendor/composer/autoload_namespaces.php @@ -2,7 +2,7 @@ // autoload_namespaces.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( diff --git a/lib/phpspreadsheet/vendor/composer/autoload_psr4.php b/lib/phpspreadsheet/vendor/composer/autoload_psr4.php index a86a0028118..b78863b7a75 100644 --- a/lib/phpspreadsheet/vendor/composer/autoload_psr4.php +++ b/lib/phpspreadsheet/vendor/composer/autoload_psr4.php @@ -2,7 +2,7 @@ // autoload_psr4.php @generated by Composer -$vendorDir = dirname(dirname(__FILE__)); +$vendorDir = dirname(__DIR__); $baseDir = dirname($vendorDir); return array( diff --git a/lib/phpspreadsheet/vendor/composer/autoload_real.php b/lib/phpspreadsheet/vendor/composer/autoload_real.php index 0745ab34d43..6c2dffc67be 100644 --- a/lib/phpspreadsheet/vendor/composer/autoload_real.php +++ b/lib/phpspreadsheet/vendor/composer/autoload_real.php @@ -2,7 +2,7 @@ // autoload_real.php @generated by Composer -class ComposerAutoloaderInit47a82a2b792e78d18b5f54d474d822dc +class ComposerAutoloaderInite0d802f9161d0d84fba97a5520240273 { private static $loader; @@ -24,31 +24,12 @@ class ComposerAutoloaderInit47a82a2b792e78d18b5f54d474d822dc require __DIR__ . '/platform_check.php'; - spl_autoload_register(array('ComposerAutoloaderInit47a82a2b792e78d18b5f54d474d822dc', 'loadClassLoader'), true, true); - self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__))); - spl_autoload_unregister(array('ComposerAutoloaderInit47a82a2b792e78d18b5f54d474d822dc', 'loadClassLoader')); + spl_autoload_register(array('ComposerAutoloaderInite0d802f9161d0d84fba97a5520240273', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__)); + spl_autoload_unregister(array('ComposerAutoloaderInite0d802f9161d0d84fba97a5520240273', 'loadClassLoader')); - $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); - if ($useStaticLoader) { - require __DIR__ . '/autoload_static.php'; - - call_user_func(\Composer\Autoload\ComposerStaticInit47a82a2b792e78d18b5f54d474d822dc::getInitializer($loader)); - } else { - $map = require __DIR__ . '/autoload_namespaces.php'; - foreach ($map as $namespace => $path) { - $loader->set($namespace, $path); - } - - $map = require __DIR__ . '/autoload_psr4.php'; - foreach ($map as $namespace => $path) { - $loader->setPsr4($namespace, $path); - } - - $classMap = require __DIR__ . '/autoload_classmap.php'; - if ($classMap) { - $loader->addClassMap($classMap); - } - } + require __DIR__ . '/autoload_static.php'; + call_user_func(\Composer\Autoload\ComposerStaticInite0d802f9161d0d84fba97a5520240273::getInitializer($loader)); $loader->register(true); diff --git a/lib/phpspreadsheet/vendor/composer/autoload_static.php b/lib/phpspreadsheet/vendor/composer/autoload_static.php index bdc8de8fb9e..116ddffb46f 100644 --- a/lib/phpspreadsheet/vendor/composer/autoload_static.php +++ b/lib/phpspreadsheet/vendor/composer/autoload_static.php @@ -4,7 +4,7 @@ namespace Composer\Autoload; -class ComposerStaticInit47a82a2b792e78d18b5f54d474d822dc +class ComposerStaticInite0d802f9161d0d84fba97a5520240273 { public static $prefixLengthsPsr4 = array ( 'P' => @@ -59,9 +59,9 @@ class ComposerStaticInit47a82a2b792e78d18b5f54d474d822dc public static function getInitializer(ClassLoader $loader) { return \Closure::bind(function () use ($loader) { - $loader->prefixLengthsPsr4 = ComposerStaticInit47a82a2b792e78d18b5f54d474d822dc::$prefixLengthsPsr4; - $loader->prefixDirsPsr4 = ComposerStaticInit47a82a2b792e78d18b5f54d474d822dc::$prefixDirsPsr4; - $loader->classMap = ComposerStaticInit47a82a2b792e78d18b5f54d474d822dc::$classMap; + $loader->prefixLengthsPsr4 = ComposerStaticInite0d802f9161d0d84fba97a5520240273::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInite0d802f9161d0d84fba97a5520240273::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInite0d802f9161d0d84fba97a5520240273::$classMap; }, null, ClassLoader::class); } diff --git a/lib/phpspreadsheet/vendor/composer/installed.json b/lib/phpspreadsheet/vendor/composer/installed.json index e9a76b7b20d..1da4ce742bf 100644 --- a/lib/phpspreadsheet/vendor/composer/installed.json +++ b/lib/phpspreadsheet/vendor/composer/installed.json @@ -115,17 +115,17 @@ }, { "name": "phpoffice/phpspreadsheet", - "version": "1.21.0", - "version_normalized": "1.21.0.0", + "version": "1.25.2", + "version_normalized": "1.25.2.0", "source": { "type": "git", "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", - "reference": "1a359d2ccbb89c05f5dffb32711a95f4afc67964" + "reference": "a317a09e7def49852400a4b3eca4a4b0790ceeb5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/1a359d2ccbb89c05f5dffb32711a95f4afc67964", - "reference": "1a359d2ccbb89c05f5dffb32711a95f4afc67964", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/a317a09e7def49852400a4b3eca4a4b0790ceeb5", + "reference": "a317a09e7def49852400a4b3eca4a4b0790ceeb5", "shasum": "" }, "require": { @@ -142,35 +142,36 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", - "ezyang/htmlpurifier": "^4.13", + "ezyang/htmlpurifier": "^4.15", "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", "php": "^7.3 || ^8.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/simple-cache": "^1.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "dompdf/dompdf": "^1.0", + "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "jpgraph/jpgraph": "^4.0", - "mpdf/mpdf": "^8.0", + "mitoteam/jpgraph": "10.2.4", + "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.6", - "tecnickcom/tcpdf": "^6.4" + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "6.5" }, "suggest": { - "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", - "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)" + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" }, - "time": "2022-01-06T11:10:08+00:00", + "time": "2022-09-25T17:21:01+00:00", "type": "library", "installation-source": "dist", "autoload": { @@ -216,7 +217,7 @@ ], "support": { "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", - "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.21.0" + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.25.2" }, "install-path": "../phpoffice/phpspreadsheet" }, diff --git a/lib/phpspreadsheet/vendor/composer/installed.php b/lib/phpspreadsheet/vendor/composer/installed.php index 91c054a37b6..c8cb02ad2b9 100644 --- a/lib/phpspreadsheet/vendor/composer/installed.php +++ b/lib/phpspreadsheet/vendor/composer/installed.php @@ -1,22 +1,22 @@ array( + 'name' => '__root__', 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', + 'reference' => NULL, 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => NULL, - 'name' => '__root__', 'dev' => true, ), 'versions' => array( '__root__' => array( 'pretty_version' => '1.0.0+no-version-set', 'version' => '1.0.0.0', + 'reference' => NULL, 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => NULL, 'dev_requirement' => false, ), 'ezyang/htmlpurifier' => array( @@ -34,19 +34,19 @@ 'markbaker/complex' => array( 'pretty_version' => '3.0.1', 'version' => '3.0.1.0', + 'reference' => 'ab8bc271e404909db09ff2d5ffa1e538085c0f22', 'type' => 'library', 'install_path' => __DIR__ . '/../markbaker/complex', 'aliases' => array(), - 'reference' => 'ab8bc271e404909db09ff2d5ffa1e538085c0f22', 'dev_requirement' => false, ), 'markbaker/matrix' => array( 'pretty_version' => '3.0.0', 'version' => '3.0.0.0', + 'reference' => 'c66aefcafb4f6c269510e9ac46b82619a904c576', 'type' => 'library', 'install_path' => __DIR__ . '/../markbaker/matrix', 'aliases' => array(), - 'reference' => 'c66aefcafb4f6c269510e9ac46b82619a904c576', 'dev_requirement' => false, ), 'myclabs/php-enum' => array( @@ -56,48 +56,48 @@ ), ), 'phpoffice/phpspreadsheet' => array( - 'pretty_version' => '1.21.0', - 'version' => '1.21.0.0', + 'pretty_version' => '1.25.2', + 'version' => '1.25.2.0', + 'reference' => 'a317a09e7def49852400a4b3eca4a4b0790ceeb5', 'type' => 'library', 'install_path' => __DIR__ . '/../phpoffice/phpspreadsheet', 'aliases' => array(), - 'reference' => '1a359d2ccbb89c05f5dffb32711a95f4afc67964', 'dev_requirement' => false, ), 'psr/http-client' => array( 'pretty_version' => '1.0.1', 'version' => '1.0.1.0', + 'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-client', 'aliases' => array(), - 'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621', 'dev_requirement' => false, ), 'psr/http-factory' => array( 'pretty_version' => '1.0.1', 'version' => '1.0.1.0', + 'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-factory', 'aliases' => array(), - 'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be', 'dev_requirement' => false, ), 'psr/http-message' => array( 'pretty_version' => '1.0.1', 'version' => '1.0.1.0', + 'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/http-message', 'aliases' => array(), - 'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363', 'dev_requirement' => false, ), 'psr/simple-cache' => array( 'pretty_version' => '1.0.1', 'version' => '1.0.1.0', + 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b', 'type' => 'library', 'install_path' => __DIR__ . '/../psr/simple-cache', 'aliases' => array(), - 'reference' => '408d5eafb83c57f6365a3ca330ff23aa4a5fa39b', 'dev_requirement' => false, ), 'symfony/polyfill-mbstring' => array( diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/CHANGELOG.md b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/CHANGELOG.md index 3bfc215a0b7..84bb504e977 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/CHANGELOG.md +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/CHANGELOG.md @@ -5,6 +5,328 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com) and this project adheres to [Semantic Versioning](https://semver.org). +## 1.25.2 - 2022-09-25 + +### Added + +- Nothing + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Composer dependency clash with ezyang/htmlpurifier + + +## 1.25.0 - 2022-09-25 + +### Added + +- Implementation of the new `TEXTBEFORE()`, `TEXTAFTER()` and `TEXTSPLIT()` Excel Functions +- Implementation of the `ARRAYTOTEXT()` and `VALUETOTEXT()` Excel Functions +- Support for [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) implementation of + JpGraph library to render charts added. +- Charts: Add Gradients, Transparency, Hidden Axes, Rounded Corners, Trendlines, Date Axes. + +### Changed + +- Allow variant behaviour when merging cells [Issue #3065](https://github.com/PHPOffice/PhpSpreadsheet/issues/3065) + - Merge methods now allow an additional `$behaviour` argument. Permitted values are: + - Worksheet::MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells (the default behaviour) + - Worksheet::MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + - Worksheet::MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell + +### Deprecated + +- Axis getLineProperty deprecated in favor of getLineColorProperty. +- Moved majorGridlines and minorGridlines from Chart to Axis. Setting either in Chart constructor or through Chart methods, or getting either using Chart methods is deprecated. +- Chart::EXCEL_COLOR_TYPE_* copied from Properties to ChartColor; use in Properties is deprecated. +- ChartColor::EXCEL_COLOR_TYPE_ARGB deprecated in favor of EXCEL_COLOR_TYPE_RGB ("A" component was never allowed). +- Misspelled Properties::LINE_STYLE_DASH_SQUERE_DOT deprecated in favor of LINE_STYLE_DASH_SQUARE_DOT. +- Clone not permitted for Spreadsheet. Spreadsheet->copy() can be used instead. + +### Removed + +- Nothing + +### Fixed + +- Fix update to defined names when inserting/deleting rows/columns [Issue #3076](https://github.com/PHPOffice/PhpSpreadsheet/issues/3076) [PR #3077](https://github.com/PHPOffice/PhpSpreadsheet/pull/3077) +- Fix DataValidation sqRef when inserting/deleting rows/columns [Issue #3056](https://github.com/PHPOffice/PhpSpreadsheet/issues/3056) [PR #3074](https://github.com/PHPOffice/PhpSpreadsheet/pull/3074) +- Named ranges not usable as anchors in OFFSET function [Issue #3013](https://github.com/PHPOffice/PhpSpreadsheet/issues/3013) +- Fully flatten an array [Issue #2955](https://github.com/PHPOffice/PhpSpreadsheet/issues/2955) [PR #2956](https://github.com/PHPOffice/PhpSpreadsheet/pull/2956) +- cellExists() and getCell() methods should support UTF-8 named cells [Issue #2987](https://github.com/PHPOffice/PhpSpreadsheet/issues/2987) [PR #2988](https://github.com/PHPOffice/PhpSpreadsheet/pull/2988) +- Spreadsheet copy fixed, clone disabled. [PR #2951](https://github.com/PHPOffice/PhpSpreadsheet/pull/2951) +- Fix PDF problems with text rotation and paper size. [Issue #1747](https://github.com/PHPOffice/PhpSpreadsheet/issues/1747) [Issue #1713](https://github.com/PHPOffice/PhpSpreadsheet/issues/1713) [PR #2960](https://github.com/PHPOffice/PhpSpreadsheet/pull/2960) +- Limited support for chart titles as formulas [Issue #2965](https://github.com/PHPOffice/PhpSpreadsheet/issues/2965) [Issue #749](https://github.com/PHPOffice/PhpSpreadsheet/issues/749) [PR #2971](https://github.com/PHPOffice/PhpSpreadsheet/pull/2971) +- Add Gradients, Transparency, and Hidden Axes to Chart [Issue #2257](https://github.com/PHPOffice/PhpSpreadsheet/issues/2257) [Issue #2229](https://github.com/PHPOffice/PhpSpreadsheet/issues/2929) [Issue #2935](https://github.com/PHPOffice/PhpSpreadsheet/issues/2935) [PR #2950](https://github.com/PHPOffice/PhpSpreadsheet/pull/2950) +- Chart Support for Rounded Corners and Trendlines [Issue #2968](https://github.com/PHPOffice/PhpSpreadsheet/issues/2968) [Issue #2815](https://github.com/PHPOffice/PhpSpreadsheet/issues/2815) [PR #2976](https://github.com/PHPOffice/PhpSpreadsheet/pull/2976) +- Add setName Method for Chart [Issue #2991](https://github.com/PHPOffice/PhpSpreadsheet/issues/2991) [PR #3001](https://github.com/PHPOffice/PhpSpreadsheet/pull/3001) +- Eliminate partial dependency on php-intl in StringHelper [Issue #2982](https://github.com/PHPOffice/PhpSpreadsheet/issues/2982) [PR #2994](https://github.com/PHPOffice/PhpSpreadsheet/pull/2994) +- Minor changes for Pdf [Issue #2999](https://github.com/PHPOffice/PhpSpreadsheet/issues/2999) [PR #3002](https://github.com/PHPOffice/PhpSpreadsheet/pull/3002) [PR #3006](https://github.com/PHPOffice/PhpSpreadsheet/pull/3006) +- Html/Pdf Do net set background color for cells using (default) nofill [PR #3016](https://github.com/PHPOffice/PhpSpreadsheet/pull/3016) +- Add support for Date Axis to Chart [Issue #2967](https://github.com/PHPOffice/PhpSpreadsheet/issues/2967) [PR #3018](https://github.com/PHPOffice/PhpSpreadsheet/pull/3018) +- Reconcile Differences Between Css and Excel for Cell Alignment [PR #3048](https://github.com/PHPOffice/PhpSpreadsheet/pull/3048) +- R1C1 Format Internationalization and Better Support for Relative Offsets [Issue #1704](https://github.com/PHPOffice/PhpSpreadsheet/issues/1704) [PR #3052](https://github.com/PHPOffice/PhpSpreadsheet/pull/3052) +- Minor Fix for Percentage Formatting [Issue #1929](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #3053](https://github.com/PHPOffice/PhpSpreadsheet/pull/3053) + +## 1.24.1 - 2022-07-18 + +### Added + +- Support for SimpleCache Interface versions 1.0, 2.0 and 3.0 +- Add Chart Axis Option textRotation [Issue #2705](https://github.com/PHPOffice/PhpSpreadsheet/issues/2705) [PR #2940](https://github.com/PHPOffice/PhpSpreadsheet/pull/2940) + +### Changed + +- Nothing + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fix Encoding issue with Html reader (PHP 8.2 deprecation for mb_convert_encoding) [Issue #2942](https://github.com/PHPOffice/PhpSpreadsheet/issues/2942) [PR #2943](https://github.com/PHPOffice/PhpSpreadsheet/pull/2943) +- Additional Chart fixes + - Pie chart with part separated unwantedly [Issue #2506](https://github.com/PHPOffice/PhpSpreadsheet/issues/2506) [PR #2928](https://github.com/PHPOffice/PhpSpreadsheet/pull/2928) + - Chart styling is lost on simple load / save process [Issue #1797](https://github.com/PHPOffice/PhpSpreadsheet/issues/1797) [Issue #2077](https://github.com/PHPOffice/PhpSpreadsheet/issues/2077) [PR #2930](https://github.com/PHPOffice/PhpSpreadsheet/pull/2930) + - Can't create contour chart (surface 2d) [Issue #2931](https://github.com/PHPOffice/PhpSpreadsheet/issues/2931) [PR #2933](https://github.com/PHPOffice/PhpSpreadsheet/pull/2933) +- VLOOKUP Breaks When Array Contains Null Cells [Issue #2934](https://github.com/PHPOffice/PhpSpreadsheet/issues/2934) [PR #2939](https://github.com/PHPOffice/PhpSpreadsheet/pull/2939) + +## 1.24.0 - 2022-07-09 + +Note that this will be the last 1.x branch release before the 2.x release. We will maintain both branches in parallel for a time; but users are requested to update to version 2.0 once that is fully available. + +### Added + +- Added `removeComment()` method for Worksheet [PR #2875](https://github.com/PHPOffice/PhpSpreadsheet/pull/2875/files) +- Add point size option for scatter charts [Issue #2298](https://github.com/PHPOffice/PhpSpreadsheet/issues/2298) [PR #2801](https://github.com/PHPOffice/PhpSpreadsheet/pull/2801) +- Basic support for Xlsx reading/writing Chart Sheets [PR #2830](https://github.com/PHPOffice/PhpSpreadsheet/pull/2830) + + Note that a ChartSheet is still only written as a normal Worksheet containing a single chart, not as an actual ChartSheet. + +- Added Worksheet visibility in Ods Reader [PR #2851](https://github.com/PHPOffice/PhpSpreadsheet/pull/2851) and Gnumeric Reader [PR #2853](https://github.com/PHPOffice/PhpSpreadsheet/pull/2853) +- Added Worksheet visibility in Ods Writer [PR #2850](https://github.com/PHPOffice/PhpSpreadsheet/pull/2850) +- Allow Csv Reader to treat string as contents of file [Issue #1285](https://github.com/PHPOffice/PhpSpreadsheet/issues/1285) [PR #2792](https://github.com/PHPOffice/PhpSpreadsheet/pull/2792) +- Allow Csv Reader to store null string rather than leave cell empty [Issue #2840](https://github.com/PHPOffice/PhpSpreadsheet/issues/2840) [PR #2842](https://github.com/PHPOffice/PhpSpreadsheet/pull/2842) +- Provide new Worksheet methods to identify if a row or column is "empty", making allowance for different definitions of "empty": + - Treat rows/columns containing no cell records as empty (default) + - Treat cells containing a null value as empty + - Treat cells containing an empty string as empty + +### Changed + +- Modify `rangeBoundaries()`, `rangeDimension()` and `getRangeBoundaries()` Coordinate methods to work with row/column ranges as well as with cell ranges and cells [PR #2926](https://github.com/PHPOffice/PhpSpreadsheet/pull/2926) +- Better enforcement of value modification to match specified datatype when using `setValueExplicit()` +- Relax validation of merge cells to allow merge for a single cell reference [Issue #2776](https://github.com/PHPOffice/PhpSpreadsheet/issues/2776) +- Memory and speed improvements, particularly for the Cell Collection, and the Writers. + + See [the Discussion section on github](https://github.com/PHPOffice/PhpSpreadsheet/discussions/2821) for details of performance across versions +- Improved performance for removing rows/columns from a worksheet + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Xls Reader resolving absolute named ranges to relative ranges [Issue #2826](https://github.com/PHPOffice/PhpSpreadsheet/issues/2826) [PR #2827](https://github.com/PHPOffice/PhpSpreadsheet/pull/2827) +- Null value handling in the Excel Math/Trig PRODUCT() function [Issue #2833](https://github.com/PHPOffice/PhpSpreadsheet/issues/2833) [PR #2834](https://github.com/PHPOffice/PhpSpreadsheet/pull/2834) +- Invalid Print Area defined in Xlsx corrupts internal storage of print area [Issue #2848](https://github.com/PHPOffice/PhpSpreadsheet/issues/2848) [PR #2849](https://github.com/PHPOffice/PhpSpreadsheet/pull/2849) +- Time interval formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) +- Copy from Xls(x) to Html/Pdf loses drawings [PR #2788](https://github.com/PHPOffice/PhpSpreadsheet/pull/2788) +- Html Reader converting cell containing 0 to null string [Issue #2810](https://github.com/PHPOffice/PhpSpreadsheet/issues/2810) [PR #2813](https://github.com/PHPOffice/PhpSpreadsheet/pull/2813) +- Many fixes for Charts, especially, but not limited to, Scatter, Bubble, and Surface charts. [Issue #2762](https://github.com/PHPOffice/PhpSpreadsheet/issues/2762) [Issue #2299](https://github.com/PHPOffice/PhpSpreadsheet/issues/2299) [Issue #2700](https://github.com/PHPOffice/PhpSpreadsheet/issues/2700) [Issue #2817](https://github.com/PHPOffice/PhpSpreadsheet/issues/2817) [Issue #2763](https://github.com/PHPOffice/PhpSpreadsheet/issues/2763) [Issue #2219](https://github.com/PHPOffice/PhpSpreadsheet/issues/2219) [Issue #2863](https://github.com/PHPOffice/PhpSpreadsheet/issues/2863) [PR #2828](https://github.com/PHPOffice/PhpSpreadsheet/pull/2828) [PR #2841](https://github.com/PHPOffice/PhpSpreadsheet/pull/2841) [PR #2846](https://github.com/PHPOffice/PhpSpreadsheet/pull/2846) [PR #2852](https://github.com/PHPOffice/PhpSpreadsheet/pull/2852) [PR #2856](https://github.com/PHPOffice/PhpSpreadsheet/pull/2856) [PR #2865](https://github.com/PHPOffice/PhpSpreadsheet/pull/2865) [PR #2872](https://github.com/PHPOffice/PhpSpreadsheet/pull/2872) [PR #2879](https://github.com/PHPOffice/PhpSpreadsheet/pull/2879) [PR #2898](https://github.com/PHPOffice/PhpSpreadsheet/pull/2898) [PR #2906](https://github.com/PHPOffice/PhpSpreadsheet/pull/2906) [PR #2922](https://github.com/PHPOffice/PhpSpreadsheet/pull/2922) [PR #2923](https://github.com/PHPOffice/PhpSpreadsheet/pull/2923) +- Adjust both coordinates for two-cell anchors when rows/columns are added/deleted. [Issue #2908](https://github.com/PHPOffice/PhpSpreadsheet/issues/2908) [PR #2909](https://github.com/PHPOffice/PhpSpreadsheet/pull/2909) +- Keep calculated string results below 32K. [PR #2921](https://github.com/PHPOffice/PhpSpreadsheet/pull/2921) +- Filter out illegal Unicode char values FFFE/FFFF. [Issue #2897](https://github.com/PHPOffice/PhpSpreadsheet/issues/2897) [PR #2910](https://github.com/PHPOffice/PhpSpreadsheet/pull/2910) +- Better handling of REF errors and propagation of all errors in Calculation engine. [PR #2902](https://github.com/PHPOffice/PhpSpreadsheet/pull/2902) +- Calculating Engine regexp for Column/Row references when there are multiple quoted worksheet references in the formula [Issue #2874](https://github.com/PHPOffice/PhpSpreadsheet/issues/2874) [PR #2899](https://github.com/PHPOffice/PhpSpreadsheet/pull/2899) + +## 1.23.0 - 2022-04-24 + +### Added + +- Ods Writer support for Freeze Pane [Issue #2013](https://github.com/PHPOffice/PhpSpreadsheet/issues/2013) [PR #2755](https://github.com/PHPOffice/PhpSpreadsheet/pull/2755) +- Ods Writer support for setting column width/row height (including the use of AutoSize) [Issue #2346](https://github.com/PHPOffice/PhpSpreadsheet/issues/2346) [PR #2753](https://github.com/PHPOffice/PhpSpreadsheet/pull/2753) +- Introduced CellAddress, CellRange, RowRange and ColumnRange value objects that can be used as an alternative to a string value (e.g. `'C5'`, `'B2:D4'`, `'2:2'` or `'B:C'`) in appropriate contexts. +- Implementation of the FILTER(), SORT(), SORTBY() and UNIQUE() Lookup/Reference (array) functions. +- Implementation of the ISREF() Information function. +- Added support for reading "formatted" numeric values from Csv files; although default behaviour of reading these values as strings is preserved. + + (i.e a value of "12,345.67" can be read as numeric `12345.67`, not simply as a string `"12,345.67"`, if the `castFormattedNumberToNumeric()` setting is enabled. + + This functionality is locale-aware, using the server's locale settings to identify the thousands and decimal separators. + +- Support for two cell anchor drawing of images. [#2532](https://github.com/PHPOffice/PhpSpreadsheet/pull/2532) [#2674](https://github.com/PHPOffice/PhpSpreadsheet/pull/2674) +- Limited support for Xls Reader to handle Conditional Formatting: + + Ranges and Rules are read, but style is currently limited to font size, weight and color; and to fill style and color. + +- Add ability to suppress Mac line ending check for CSV [#2623](https://github.com/PHPOffice/PhpSpreadsheet/pull/2623) +- Initial support for creating and writing Tables (Xlsx Writer only) [PR #2671](https://github.com/PHPOffice/PhpSpreadsheet/pull/2671) + + See `/samples/Table` for examples of use. + + Note that PreCalculateFormulas needs to be disabled when saving spreadsheets containing tables with formulae (totals or column formulae). + +### Changed + +- Gnumeric Reader now loads number formatting for cells. +- Gnumeric Reader now correctly identifies selected worksheet and selected cells in a worksheet. +- Some Refactoring of the Ods Reader, moving all formula and address translation from Ods to Excel into a separate class to eliminate code duplication and ensure consistency. +- Make Boolean Conversion in Csv Reader locale-aware when using the String Value Binder. + + This is determined by the Calculation Engine locale setting. + + (i.e. `"Vrai"` wil be converted to a boolean `true` if the Locale is set to `fr`.) +- Allow `psr/simple-cache` 2.x + +### Deprecated + +- All Excel Function implementations in `Calculation\Functions` (including the Error functions) have been moved to dedicated classes for groups of related functions. See the docblocks against all the deprecated methods for details of the new methods to call instead. At some point, these old classes will be deleted. +- Worksheet methods that reference cells "byColumnandRow". All such methods have an equivalent that references the cell by its address (e.g. '`E3'` rather than `5, 3`). + + These functions now accept either a cell address string (`'E3')` or an array with columnId and rowId (`[5, 3]`) or a new `CellAddress` object as their `cellAddress`/`coordinate` argument. + This includes the methods: + - `setCellValueByColumnAndRow()` use the equivalent `setCellValue()` + - `setCellValueExplicitByColumnAndRow()` use the equivalent `setCellValueExplicit()` + - `getCellByColumnAndRow()` use the equivalent `getCell()` + - `cellExistsByColumnAndRow()` use the equivalent `cellExists()` + - `getStyleByColumnAndRow()` use the equivalent `getStyle()` + - `setBreakByColumnAndRow()` use the equivalent `setBreak()` + - `mergeCellsByColumnAndRow()` use the equivalent `mergeCells()` + - `unmergeCellsByColumnAndRow()` use the equivalent `unmergeCells()` + - `protectCellsByColumnAndRow()` use the equivalent `protectCells()` + - `unprotectCellsByColumnAndRow()` use the equivalent `unprotectCells()` + - `setAutoFilterByColumnAndRow()` use the equivalent `setAutoFilter()` + - `freezePaneByColumnAndRow()` use the equivalent `freezePane()` + - `getCommentByColumnAndRow()` use the equivalent `getComment()` + - `setSelectedCellByColumnAndRow()` use the equivalent `setSelectedCells()` + + This change provides more consistency in the methods (not every "by cell address" method has an equivalent "byColumnAndRow" method); + and the "by cell address" methods often provide more flexibility, such as allowing a range of cells, or referencing them by passing the defined name of a named range as the argument. + +### Removed + +- Nothing + +### Fixed + +- Make allowance for the AutoFilter dropdown icon in the first row of an Autofilter range when using Autosize columns. [Issue #2413](https://github.com/PHPOffice/PhpSpreadsheet/issues/2413) [PR #2754](https://github.com/PHPOffice/PhpSpreadsheet/pull/2754) +- Support for "chained" ranges (e.g. `A5:C10:C20:F1`) in the Calculation Engine; and also support for using named ranges with the Range operator (e.g. `NamedRange1:NamedRange2`) [Issue #2730](https://github.com/PHPOffice/PhpSpreadsheet/issues/2730) [PR #2746](https://github.com/PHPOffice/PhpSpreadsheet/pull/2746) +- Update Conditional Formatting ranges and rule conditions when inserting/deleting rows/columns [Issue #2678](https://github.com/PHPOffice/PhpSpreadsheet/issues/2678) [PR #2689](https://github.com/PHPOffice/PhpSpreadsheet/pull/2689) +- Allow `INDIRECT()` to accept row/column ranges as well as cell ranges [PR #2687](https://github.com/PHPOffice/PhpSpreadsheet/pull/2687) +- Fix bug when deleting cells with hyperlinks, where the hyperlink was then being "inherited" by whatever cell moved to that cell address. +- Fix bug in Conditional Formatting in the Xls Writer that resulted in a broken file when there were multiple conditional ranges in a worksheet. +- Fix Conditional Formatting in the Xls Writer to work with rules that contain string literals, cell references and formulae. +- Fix for setting Active Sheet to the first loaded worksheet when bookViews element isn't defined [Issue #2666](https://github.com/PHPOffice/PhpSpreadsheet/issues/2666) [PR #2669](https://github.com/PHPOffice/PhpSpreadsheet/pull/2669) +- Fixed behaviour of XLSX font style vertical align settings [PR #2619](https://github.com/PHPOffice/PhpSpreadsheet/pull/2619) +- Resolved formula translations to handle separators (row and column) for array functions as well as for function argument separators; and cleanly handle nesting levels. + + Note that this method is used when translating Excel functions between `en_us` and other locale languages, as well as when converting formulae between different spreadsheet formats (e.g. Ods to Excel). + + Nor is this a perfect solution, as there may still be issues when function calls have array arguments that themselves contain function calls; but it's still better than the current logic. +- Fix for escaping double quotes within a formula [Issue #1971](https://github.com/PHPOffice/PhpSpreadsheet/issues/1971) [PR #2651](https://github.com/PHPOffice/PhpSpreadsheet/pull/2651) +- Change open mode for output from `wb+` to `wb` [Issue #2372](https://github.com/PHPOffice/PhpSpreadsheet/issues/2372) [PR #2657](https://github.com/PHPOffice/PhpSpreadsheet/pull/2657) +- Use color palette if supplied [Issue #2499](https://github.com/PHPOffice/PhpSpreadsheet/issues/2499) [PR #2595](https://github.com/PHPOffice/PhpSpreadsheet/pull/2595) +- Xls reader treat drawing offsets as int rather than float [PR #2648](https://github.com/PHPOffice/PhpSpreadsheet/pull/2648) +- Handle booleans in conditional styles properly [PR #2654](https://github.com/PHPOffice/PhpSpreadsheet/pull/2654) +- Fix for reading files in the root directory of a ZipFile, which should not be prefixed by relative paths ("./") as dirname($filename) does by default. +- Fix invalid style of cells in empty columns with columnDimensions and rows with rowDimensions in added external sheet. [PR #2739](https://github.com/PHPOffice/PhpSpreadsheet/pull/2739) +- Time Interval Formatting [Issue #2768](https://github.com/PHPOffice/PhpSpreadsheet/issues/2768) [PR #2772](https://github.com/PHPOffice/PhpSpreadsheet/pull/2772) + +## 1.22.0 - 2022-02-18 + +### Added + +- Namespacing phase 2 - styles. +[PR #2471](https://github.com/PHPOffice/PhpSpreadsheet/pull/2471) + +- Improved support for passing of array arguments to Excel function implementations to return array results (where appropriate). [Issue #2551](https://github.com/PHPOffice/PhpSpreadsheet/issues/2551) + + This is the first stage in an ongoing process of adding array support to all appropriate function implementations, +- Support for the Excel365 Math/Trig SEQUENCE() function [PR #2536](https://github.com/PHPOffice/PhpSpreadsheet/pull/2536) +- Support for the Excel365 Math/Trig RANDARRAY() function [PR #2540](https://github.com/PHPOffice/PhpSpreadsheet/pull/2540) + + Note that the Spill Operator is not yet supported in the Calculation Engine; but this can still be useful for defining array constants. +- Improved support for Conditional Formatting Rules [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491) + - Provide support for a wider range of Conditional Formatting Rules for Xlsx Reader/Writer: + - Cells Containing (cellIs) + - Specific Text (containing, notContaining, beginsWith, endsWith) + - Dates Occurring (all supported timePeriods) + - Blanks/NoBlanks + - Errors/NoErrors + - Duplicates/Unique + - Expression + - Provision of CF Wizards (for all the above listed rule types) to help create/modify CF Rules without having to manage all the combinations of types/operators, and the complexities of formula expressions, or the text/timePeriod attributes. + + See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/conditional-formatting/) for details + + - Full support of the above CF Rules for the Xlsx Reader and Writer; even when the file being loaded has CF rules listed in the `` element for the worksheet rather than the `` element. + - Provision of a CellMatcher to identify if rules are matched for a cell, and which matching style will be applied. + - Improved documentation and examples, covering all supported CF rule types. + - Add support for one digit decimals (FORMAT_NUMBER_0, FORMAT_PERCENTAGE_0). [PR #2525](https://github.com/PHPOffice/PhpSpreadsheet/pull/2525) + - Initial work enabling Excel function implementations for handling arrays as arguments when used in "array formulae" [#2562](https://github.com/PHPOffice/PhpSpreadsheet/issues/2562) + - Enable most of the Date/Time functions to accept array arguments [#2573](https://github.com/PHPOffice/PhpSpreadsheet/issues/2573) + - Array ready functions - Text, Math/Trig, Statistical, Engineering and Logical [#2580](https://github.com/PHPOffice/PhpSpreadsheet/issues/2580) + +### Changed + +- Additional Russian translations for Excel Functions (courtesy of aleks-samurai). +- Improved code coverage for NumberFormat. [PR #2556](https://github.com/PHPOffice/PhpSpreadsheet/pull/2556) +- Extract some methods from the Calculation Engine into dedicated classes [#2537](https://github.com/PHPOffice/PhpSpreadsheet/issues/2537) +- Eliminate calls to `flattenSingleValue()` that are no longer required when we're checking for array values as arguments [#2590](https://github.com/PHPOffice/PhpSpreadsheet/issues/2590) + +### Deprecated + +- Nothing + +### Removed + +- Nothing + +### Fixed + +- Fixed `ReferenceHelper@insertNewBefore` behavior when removing column before last column with null value [PR #2541](https://github.com/PHPOffice/PhpSpreadsheet/pull/2541) +- Fix bug with `DOLLARDE()` and `DOLLARFR()` functions when the dollar value is negative [Issue #2578](https://github.com/PHPOffice/PhpSpreadsheet/issues/2578) [PR #2579](https://github.com/PHPOffice/PhpSpreadsheet/pull/2579) +- Fix partial function name matching when translating formulae from Russian to English [Issue #2533](https://github.com/PHPOffice/PhpSpreadsheet/issues/2533) [PR #2534](https://github.com/PHPOffice/PhpSpreadsheet/pull/2534) +- Various bugs related to Conditional Formatting Rules, and errors in the Xlsx Writer for Conditional Formatting [PR #2491](https://github.com/PHPOffice/PhpSpreadsheet/pull/2491) +- Xlsx Reader merge range fixes. [Issue #2501](https://github.com/PHPOffice/PhpSpreadsheet/issues/2501) [PR #2504](https://github.com/PHPOffice/PhpSpreadsheet/pull/2504) +- Handle explicit "date" type for Cell in Xlsx Reader. [Issue #2373](https://github.com/PHPOffice/PhpSpreadsheet/issues/2373) [PR #2485](https://github.com/PHPOffice/PhpSpreadsheet/pull/2485) +- Recalibrate Row/Column Dimensions after removeRow/Column. [Issue #2442](https://github.com/PHPOffice/PhpSpreadsheet/issues/2442) [PR #2486](https://github.com/PHPOffice/PhpSpreadsheet/pull/2486) +- Refinement for XIRR. [Issue #2469](https://github.com/PHPOffice/PhpSpreadsheet/issues/2469) [PR #2487](https://github.com/PHPOffice/PhpSpreadsheet/pull/2487) +- Xlsx Reader handle cell with non-null explicit type but null value. [Issue #2488](https://github.com/PHPOffice/PhpSpreadsheet/issues/2488) [PR #2489](https://github.com/PHPOffice/PhpSpreadsheet/pull/2489) +- Xlsx Reader fix height and width for oneCellAnchorDrawings. [PR #2492](https://github.com/PHPOffice/PhpSpreadsheet/pull/2492) +- Fix rounding error in NumberFormat::NUMBER_PERCENTAGE, NumberFormat::NUMBER_PERCENTAGE_00. [PR #2555](https://github.com/PHPOffice/PhpSpreadsheet/pull/2555) +- Don't treat thumbnail file as xml. [Issue #2516](https://github.com/PHPOffice/PhpSpreadsheet/issues/2516) [PR #2517](https://github.com/PHPOffice/PhpSpreadsheet/pull/2517) +- Eliminating Xlsx Reader warning when no sz tag for RichText. [Issue #2542](https://github.com/PHPOffice/PhpSpreadsheet/issues/2542) [PR #2550](https://github.com/PHPOffice/PhpSpreadsheet/pull/2550) +- Fix Xlsx/Xls Writer handling of inline strings. [Issue #353](https://github.com/PHPOffice/PhpSpreadsheet/issues/353) [PR #2569](https://github.com/PHPOffice/PhpSpreadsheet/pull/2569) +- Richtext colors were not being read correctly after namespace change [#2458](https://github.com/PHPOffice/PhpSpreadsheet/issues/2458) +- Fix discrepancy between the way markdown tables are rendered in ReadTheDocs and in PHPStorm [#2520](https://github.com/PHPOffice/PhpSpreadsheet/issues/2520) +- Update Russian Functions Text File [#2557](https://github.com/PHPOffice/PhpSpreadsheet/issues/2557) +- Fix documentation, instantiation example [#2564](https://github.com/PHPOffice/PhpSpreadsheet/issues/2564) + + ## 1.21.0 - 2022-01-06 ### Added diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/README.md b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/README.md index 2a94e0d3d56..ef04cd072f4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/README.md +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/README.md @@ -11,6 +11,80 @@ PhpSpreadsheet is a library written in pure PHP and offers a set of classes that allow you to read and write various spreadsheet file formats such as Excel and LibreOffice Calc. +## PHP version support + +LTS: Support for PHP versions will only be maintained for a period of six months beyond the +[end of life of that PHP version](https://www.php.net/eol.php). + +Currently the required PHP minimum version is PHP __7.3__. + +See the `composer.json` for other requirements. + +## Installation + +Use [composer](https://getcomposer.org) to install PhpSpreadsheet into your project: + +```sh +composer require phpoffice/phpspreadsheet +``` + +If you are building your installation on a development machine that is on a different PHP version to the server where it will be deployed, or if your PHP CLI version is not the same as your run-time such as `php-fpm` or Apache's `mod_php`, then you might want to add the following to your `composer.json` before installing: +```json +{ + "require": { + "phpoffice/phpspreadsheet": "^1.23" + }, + "config": { + "platform": { + "php": "7.3" + } + } +} +``` +and then run +```sh +composer install +``` +to ensure that the correct dependencies are retrieved to match your deployment environment. + +See [CLI vs Application run-time](https://php.watch/articles/composer-platform-check) for more details. + +### Additional Installation Options + +If you want to write to PDF, or to include Charts when you write to HTML or PDF, then you will need to install additional libraries: + +#### PDF + +For PDF Generation, you can install any of the following, and then configure PhpSpreadsheet to indicate which library you are going to use: + - mpdf/mpdf + - dompdf/dompdf + - tecnickcom/tcpdf + +and configure PhpSpreadsheet using: + +```php +// Dompdf, Mpdf or Tcpdf (as appropriate) +$className = \PhpOffice\PhpSpreadsheet\Writer\Pdf\Dompdf::class; +IOFactory::registerWriter('Pdf', $className); +``` +or the appropriate PDF Writer wrapper for the library that you have chosen to install. + +#### Chart Export + +For Chart export, we support following packages, which you will also need to install yourself using `composer require` + - [jpgraph/jpgraph](https://packagist.org/packages/jpgraph/jpgraph) (this package was abandoned at version 4.0. + You can manually download the latest version that supports PHP 8 and above from [jpgraph.net](https://jpgraph.net/)) + - [mitoteam/jpgraph](https://packagist.org/packages/mitoteam/jpgraph) (fork with php 8.1 support) + +and then configure PhpSpreadsheet using: +```php +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); // to use jpgraph/jpgraph +//or +Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\MtJpGraphRenderer::class); // to use mitoteam/jpgraph +``` + +One or the other of these libraries is necessary if you want to generate HTML or PDF files that include charts. + ## Documentation Read more about it, including install instructions, in the [official documentation](https://phpspreadsheet.readthedocs.io). Or check out the [API documentation](https://phpoffice.github.io/PhpSpreadsheet). diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/composer.json b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/composer.json index d80fc62ffab..e686788eb2b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/composer.json +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/composer.json @@ -12,7 +12,10 @@ "spreadsheet" ], "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } }, "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", "type": "library", @@ -66,32 +69,33 @@ "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", - "ezyang/htmlpurifier": "^4.13", + "ezyang/htmlpurifier": "^4.15", "maennchen/zipstream-php": "^2.1", "markbaker/complex": "^3.0", "markbaker/matrix": "^3.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "psr/simple-cache": "^1.0" + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" }, "require-dev": { "dealerdirect/phpcodesniffer-composer-installer": "dev-master", - "dompdf/dompdf": "^1.0", + "dompdf/dompdf": "^1.0 || ^2.0", "friendsofphp/php-cs-fixer": "^3.2", - "jpgraph/jpgraph": "^4.0", - "mpdf/mpdf": "^8.0", + "mitoteam/jpgraph": "10.2.4", + "mpdf/mpdf": "8.1.1", "phpcompatibility/php-compatibility": "^9.3", "phpstan/phpstan": "^1.1", "phpstan/phpstan-phpunit": "^1.0", "phpunit/phpunit": "^8.5 || ^9.0", - "squizlabs/php_codesniffer": "^3.6", - "tecnickcom/tcpdf": "^6.4" + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "6.5" }, "suggest": { + "ext-intl": "PHP Internationalization Functions", "mpdf/mpdf": "Option for rendering PDF with PDF Writer", - "dompdf/dompdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", - "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer (doesn't yet support PHP8)", - "jpgraph/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers" }, "autoload": { "psr-4": { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ArrayEnabled.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ArrayEnabled.php new file mode 100644 index 00000000000..1e3f6971df6 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ArrayEnabled.php @@ -0,0 +1,133 @@ +initialise(($arguments === false) ? [] : $arguments); + } + + /** + * Handles array argument processing when the function accepts a single argument that can be an array argument. + * Example use for: + * DAYOFMONTH() or FACT(). + */ + protected static function evaluateSingleArgumentArray(callable $method, array $values): array + { + $result = []; + foreach ($values as $value) { + $result[] = $method($value); + } + + return $result; + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * and any of them can be an array argument. + * Example use for: + * ROUND() or DATE(). + * + * @param mixed ...$arguments + */ + protected static function evaluateArrayArguments(callable $method, ...$arguments): array + { + self::initialiseHelper($arguments); + $arguments = self::$arrayArgumentHelper->arguments(); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * but only the first few (up to limit) can be an array arguments. + * Example use for: + * NETWORKDAYS() or CONCATENATE(), where the last argument is a matrix (or a series of values) that need + * to be treated as a such rather than as an array arguments. + * + * @param mixed ...$arguments + */ + protected static function evaluateArrayArgumentsSubset(callable $method, int $limit, ...$arguments): array + { + self::initialiseHelper(array_slice($arguments, 0, $limit)); + $trailingArguments = array_slice($arguments, $limit); + $arguments = self::$arrayArgumentHelper->arguments(); + $arguments = array_merge($arguments, $trailingArguments); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } + + /** + * @param mixed $value + */ + private static function testFalse($value): bool + { + return $value === false; + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * but only the last few (from start) can be an array arguments. + * Example use for: + * Z.TEST() or INDEX(), where the first argument 1 is a matrix that needs to be treated as a dataset + * rather than as an array argument. + * + * @param mixed ...$arguments + */ + protected static function evaluateArrayArgumentsSubsetFrom(callable $method, int $start, ...$arguments): array + { + $arrayArgumentsSubset = array_combine( + range($start, count($arguments) - $start), + array_slice($arguments, $start) + ); + if (self::testFalse($arrayArgumentsSubset)) { + return ['#VALUE!']; + } + + self::initialiseHelper($arrayArgumentsSubset); + $leadingArguments = array_slice($arguments, 0, $start); + $arguments = self::$arrayArgumentHelper->arguments(); + $arguments = array_merge($leadingArguments, $arguments); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } + + /** + * Handles array argument processing when the function accepts multiple arguments, + * and any of them can be an array argument except for the one specified by ignore. + * Example use for: + * HLOOKUP() and VLOOKUP(), where argument 1 is a matrix that needs to be treated as a database + * rather than as an array argument. + * + * @param mixed ...$arguments + */ + protected static function evaluateArrayArgumentsIgnore(callable $method, int $ignore, ...$arguments): array + { + $leadingArguments = array_slice($arguments, 0, $ignore); + $ignoreArgument = array_slice($arguments, $ignore, 1); + $trailingArguments = array_slice($arguments, $ignore + 1); + + self::initialiseHelper(array_merge($leadingArguments, [[null]], $trailingArguments)); + $arguments = self::$arrayArgumentHelper->arguments(); + + array_splice($arguments, $ignore, 1, $ignoreArgument); + + return ArrayArgumentProcessor::processArguments(self::$arrayArgumentHelper, $method, ...$arguments); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php new file mode 100644 index 00000000000..5d4f261fc16 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/BinaryComparison.php @@ -0,0 +1,181 @@ + '' && $operand1[0] == Calculation::FORMULA_STRING_QUOTE) { + $operand1 = Calculation::unwrapResult($operand1); + } + if (is_string($operand2) && $operand2 > '' && $operand2[0] == Calculation::FORMULA_STRING_QUOTE) { + $operand2 = Calculation::unwrapResult($operand2); + } + + // Use case insensitive comparaison if not OpenOffice mode + if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) { + if (is_string($operand1)) { + $operand1 = StringHelper::strToUpper($operand1); + } + if (is_string($operand2)) { + $operand2 = StringHelper::strToUpper($operand2); + } + } + + $useLowercaseFirstComparison = is_string($operand1) && + is_string($operand2) && + Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE; + + return self::evaluateComparison($operand1, $operand2, $operator, $useLowercaseFirstComparison); + } + + /** + * @param mixed $operand1 + * @param mixed $operand2 + */ + private static function evaluateComparison($operand1, $operand2, string $operator, bool $useLowercaseFirstComparison): bool + { + switch ($operator) { + // Equality + case '=': + return self::equal($operand1, $operand2); + // Greater than + case '>': + return self::greaterThan($operand1, $operand2, $useLowercaseFirstComparison); + // Less than + case '<': + return self::lessThan($operand1, $operand2, $useLowercaseFirstComparison); + // Greater than or equal + case '>=': + return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison); + // Less than or equal + case '<=': + return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison); + // Inequality + case '<>': + return self::notEqual($operand1, $operand2); + default: + throw new Exception('Unsupported binary comparison operator'); + } + } + + /** + * @param mixed $operand1 + * @param mixed $operand2 + */ + private static function equal($operand1, $operand2): bool + { + if (is_numeric($operand1) && is_numeric($operand2)) { + $result = (abs($operand1 - $operand2) < self::DELTA); + } elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { + $result = $operand1 == $operand2; + } else { + $result = self::strcmpAllowNull($operand1, $operand2) == 0; + } + + return $result; + } + + /** + * @param mixed $operand1 + * @param mixed $operand2 + */ + private static function greaterThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool + { + if (is_numeric($operand1) && is_numeric($operand2)) { + $result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 > $operand2)); + } elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { + $result = $operand1 >= $operand2; + } elseif ($useLowercaseFirstComparison) { + $result = self::strcmpLowercaseFirst($operand1, $operand2) >= 0; + } else { + $result = self::strcmpAllowNull($operand1, $operand2) >= 0; + } + + return $result; + } + + /** + * @param mixed $operand1 + * @param mixed $operand2 + */ + private static function lessThanOrEqual($operand1, $operand2, bool $useLowercaseFirstComparison): bool + { + if (is_numeric($operand1) && is_numeric($operand2)) { + $result = ((abs($operand1 - $operand2) < self::DELTA) || ($operand1 < $operand2)); + } elseif (($operand1 === null && is_numeric($operand2)) || ($operand2 === null && is_numeric($operand1))) { + $result = $operand1 <= $operand2; + } elseif ($useLowercaseFirstComparison) { + $result = self::strcmpLowercaseFirst($operand1, $operand2) <= 0; + } else { + $result = self::strcmpAllowNull($operand1, $operand2) <= 0; + } + + return $result; + } + + /** + * @param mixed $operand1 + * @param mixed $operand2 + */ + private static function greaterThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool + { + return self::lessThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true; + } + + /** + * @param mixed $operand1 + * @param mixed $operand2 + */ + private static function lessThan($operand1, $operand2, bool $useLowercaseFirstComparison): bool + { + return self::greaterThanOrEqual($operand1, $operand2, $useLowercaseFirstComparison) !== true; + } + + /** + * @param mixed $operand1 + * @param mixed $operand2 + */ + private static function notEqual($operand1, $operand2): bool + { + return self::equal($operand1, $operand2) !== true; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php index 25f7695904c..4f95af54f26 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner; use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack; use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\Shared; @@ -29,17 +33,17 @@ class Calculation // Function (allow for the old @ symbol that could be used to prefix a function, but we'll ignore it) const CALCULATION_REGEXP_FUNCTION = '@?(?:_xlfn\.)?([\p{L}][\p{L}\p{N}\.]*)[\s]*\('; // Cell reference (cell or range of cells, with or without a sheet reference) - const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_CELLREF = '((([^\s,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?\$?\b([a-z]{1,3})\$?(\d{1,7})(?![\w.])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative - const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; - const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?(\$?[a-z]{1,3})):(?![.*])'; - const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; + const CALCULATION_REGEXP_CELLREF_RELATIVE = '((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?\b[a-z]{1,3})(\$?\d{1,7})(?![\w.])'; + const CALCULATION_REGEXP_COLUMN_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\".(?:[^\"]|\"[^!])?\"))!)?(\$?[a-z]{1,3})):(?![.*])'; + const CALCULATION_REGEXP_ROW_RANGE = '(((([^\s\(,!&%^\/\*\+<>=:`-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?(\$?[1-9][0-9]{0,6})):(?![.*])'; // Cell reference (with or without a sheet reference) ensuring absolute/relative // Cell ranges ensuring absolute/relative const CALCULATION_REGEXP_COLUMNRANGE_RELATIVE = '(\$?[a-z]{1,3}):(\$?[a-z]{1,3})'; const CALCULATION_REGEXP_ROWRANGE_RELATIVE = '(\$?\d{1,7}):(\$?\d{1,7})'; // Defined Names: Named Range of cells, or Named Formulae - const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'[^\']*\')|(\"[^\"]*\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; + const CALCULATION_REGEXP_DEFINEDNAME = '((([^\s,!&%^\/\*\+<>=-]*)|(\'(?:[^\']|\'[^!])+?\')|(\"(?:[^\"]|\"[^!])+?\"))!)?([_\p{L}][_\p{L}\p{N}\.]*)'; // Error const CALCULATION_REGEXP_ERROR = '\#[A-Z][A-Z0_\/]*[!\?]?'; @@ -48,8 +52,10 @@ class Calculation const RETURN_ARRAY_AS_VALUE = 'value'; const RETURN_ARRAY_AS_ARRAY = 'array'; - const FORMULA_OPEN_FUNCTION_BRACE = '{'; - const FORMULA_CLOSE_FUNCTION_BRACE = '}'; + const FORMULA_OPEN_FUNCTION_BRACE = '('; + const FORMULA_CLOSE_FUNCTION_BRACE = ')'; + const FORMULA_OPEN_MATRIX_BRACE = '{'; + const FORMULA_CLOSE_MATRIX_BRACE = '}'; const FORMULA_STRING_QUOTE = '"'; private static $returnArrayAsType = self::RETURN_ARRAY_AS_VALUE; @@ -83,12 +89,13 @@ class Calculation private $calculationCacheEnabled = true; /** - * Used to generate unique store keys. - * - * @var int + * @var BranchPruner */ - private $branchStoreKeyCounter = 0; + private $branchPruner; + /** + * @var bool + */ private $branchPruningEnabled = true; /** @@ -101,7 +108,8 @@ class Calculation '+' => true, '-' => true, '*' => true, '/' => true, '^' => true, '&' => true, '%' => false, '~' => false, '>' => true, '<' => true, '=' => true, '>=' => true, - '<=' => true, '<>' => true, '|' => true, ':' => true, + '<=' => true, '<>' => true, '∩' => true, '∪' => true, + ':' => true, ]; /** @@ -113,7 +121,7 @@ class Calculation '+' => true, '-' => true, '*' => true, '/' => true, '^' => true, '&' => true, '>' => true, '<' => true, '=' => true, '>=' => true, '<=' => true, '<>' => true, - '|' => true, ':' => true, + '∩' => true, '∪' => true, ':' => true, ]; /** @@ -173,13 +181,6 @@ class Calculation */ public $cyclicFormulaCount = 1; - /** - * Epsilon Precision used for comparisons in calculations. - * - * @var float - */ - private $delta = 0.1e-12; - /** * The current locale setting. * @@ -223,7 +224,7 @@ class Calculation * * @var array */ - private static $excelConstants = [ + public static $excelConstants = [ 'TRUE' => true, 'FALSE' => false, 'NULL' => null, @@ -303,8 +304,8 @@ class Calculation ], 'ARRAYTOTEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '?', + 'functionCall' => [TextData\Text::class, 'fromArray'], + 'argumentCount' => '1,2', ], 'ASC' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, @@ -541,6 +542,16 @@ class Calculation 'functionCall' => [LookupRef\Selection::class, 'CHOOSE'], 'argumentCount' => '2+', ], + 'CHOOSECOLS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2+', + ], + 'CHOOSEROWS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2+', + ], 'CLEAN' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, 'functionCall' => [TextData\Trim::class, 'nonPrintable'], @@ -903,6 +914,11 @@ class Calculation 'functionCall' => [Database\DProduct::class, 'evaluate'], 'argumentCount' => '3', ], + 'DROP' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-3', + ], 'DSTDEV' => [ 'category' => Category::CATEGORY_DATABASE, 'functionCall' => [Database\DStDev::class, 'evaluate'], @@ -980,7 +996,7 @@ class Calculation ], 'ERROR.TYPE' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'errorType'], + 'functionCall' => [Information\ExcelError::class, 'type'], 'argumentCount' => '1', ], 'EVEN' => [ @@ -998,6 +1014,11 @@ class Calculation 'functionCall' => [MathTrig\Exp::class, 'evaluate'], 'argumentCount' => '1', ], + 'EXPAND' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-4', + ], 'EXPONDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, 'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'], @@ -1040,8 +1061,8 @@ class Calculation ], 'FILTER' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '3+', + 'functionCall' => [LookupRef\Filter::class, 'filter'], + 'argumentCount' => '2-3', ], 'FILTERXML' => [ 'category' => Category::CATEGORY_WEB, @@ -1260,6 +1281,11 @@ class Calculation 'functionCall' => [DateTimeExcel\TimeParts::class, 'hour'], 'argumentCount' => '1', ], + 'HSTACK' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1+', + ], 'HYPERLINK' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, 'functionCall' => [LookupRef\Hyperlink::class, 'set'], @@ -1464,49 +1490,49 @@ class Calculation ], 'ISBLANK' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isBlank'], + 'functionCall' => [Information\Value::class, 'isBlank'], 'argumentCount' => '1', ], 'ISERR' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isErr'], + 'functionCall' => [Information\ErrorValue::class, 'isErr'], 'argumentCount' => '1', ], 'ISERROR' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isError'], + 'functionCall' => [Information\ErrorValue::class, 'isError'], 'argumentCount' => '1', ], 'ISEVEN' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isEven'], + 'functionCall' => [Information\Value::class, 'isEven'], 'argumentCount' => '1', ], 'ISFORMULA' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isFormula'], + 'functionCall' => [Information\Value::class, 'isFormula'], 'argumentCount' => '1', 'passCellReference' => true, 'passByReference' => [true], ], 'ISLOGICAL' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isLogical'], + 'functionCall' => [Information\Value::class, 'isLogical'], 'argumentCount' => '1', ], 'ISNA' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isNa'], + 'functionCall' => [Information\ErrorValue::class, 'isNa'], 'argumentCount' => '1', ], 'ISNONTEXT' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isNonText'], + 'functionCall' => [Information\Value::class, 'isNonText'], 'argumentCount' => '1', ], 'ISNUMBER' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isNumber'], + 'functionCall' => [Information\Value::class, 'isNumber'], 'argumentCount' => '1', ], 'ISO.CEILING' => [ @@ -1516,7 +1542,7 @@ class Calculation ], 'ISODD' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isOdd'], + 'functionCall' => [Information\Value::class, 'isOdd'], 'argumentCount' => '1', ], 'ISOWEEKNUM' => [ @@ -1531,12 +1557,14 @@ class Calculation ], 'ISREF' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Information\Value::class, 'isRef'], 'argumentCount' => '1', + 'passCellReference' => true, + 'passByReference' => [true], ], 'ISTEXT' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'isText'], + 'functionCall' => [Information\Value::class, 'isText'], 'argumentCount' => '1', ], 'ISTHAIDIGIT' => [ @@ -1766,12 +1794,12 @@ class Calculation ], 'N' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'n'], + 'functionCall' => [Information\Value::class, 'asNumber'], 'argumentCount' => '1', ], 'NA' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'NA'], + 'functionCall' => [Information\ExcelError::class, 'NA'], 'argumentCount' => '0', ], 'NEGBINOMDIST' => [ @@ -2078,7 +2106,7 @@ class Calculation ], 'RANDARRAY' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [MathTrig\Random::class, 'randArray'], 'argumentCount' => '0-5', ], 'RANDBETWEEN' => [ @@ -2220,8 +2248,8 @@ class Calculation ], 'SEQUENCE' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '2', + 'functionCall' => [MathTrig\MatrixFunctions::class, 'sequence'], + 'argumentCount' => '1-4', ], 'SERIESSUM' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, @@ -2280,12 +2308,12 @@ class Calculation ], 'SORT' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '1+', + 'functionCall' => [LookupRef\Sort::class, 'sort'], + 'argumentCount' => '1-4', ], 'SORTBY' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Sort::class, 'sortBy'], 'argumentCount' => '2+', ], 'SQRT' => [ @@ -2404,6 +2432,11 @@ class Calculation 'functionCall' => [TextData\Text::class, 'test'], 'argumentCount' => '1', ], + 'TAKE' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-3', + ], 'TAN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, 'functionCall' => [MathTrig\Trig\Tangent::class, 'tan'], @@ -2454,11 +2487,26 @@ class Calculation 'functionCall' => [TextData\Format::class, 'TEXTFORMAT'], 'argumentCount' => '2', ], + 'TEXTAFTER' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'after'], + 'argumentCount' => '2-6', + ], + 'TEXTBEFORE' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Extract::class, 'before'], + 'argumentCount' => '2-6', + ], 'TEXTJOIN' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, 'functionCall' => [TextData\Concatenate::class, 'TEXTJOIN'], 'argumentCount' => '3+', ], + 'TEXTSPLIT' => [ + 'category' => Category::CATEGORY_TEXT_AND_DATA, + 'functionCall' => [TextData\Text::class, 'split'], + 'argumentCount' => '2-6', + ], 'THAIDAYOFWEEK' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, 'functionCall' => [Functions::class, 'DUMMY'], @@ -2524,6 +2572,16 @@ class Calculation 'functionCall' => [DateTimeExcel\Current::class, 'today'], 'argumentCount' => '0', ], + 'TOCOL' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1-3', + ], + 'TOROW' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1-3', + ], 'TRANSPOSE' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, 'functionCall' => [LookupRef\Matrix::class, 'transpose'], @@ -2566,7 +2624,7 @@ class Calculation ], 'TYPE' => [ 'category' => Category::CATEGORY_INFORMATION, - 'functionCall' => [Functions::class, 'TYPE'], + 'functionCall' => [Information\Value::class, 'type'], 'argumentCount' => '1', ], 'UNICHAR' => [ @@ -2581,7 +2639,7 @@ class Calculation ], 'UNIQUE' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [LookupRef\Unique::class, 'unique'], 'argumentCount' => '1+', ], 'UPPER' => [ @@ -2601,8 +2659,8 @@ class Calculation ], 'VALUETOTEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [Functions::class, 'DUMMY'], - 'argumentCount' => '?', + 'functionCall' => [TextData\Format::class, 'valueToText'], + 'argumentCount' => '1,2', ], 'VAR' => [ 'category' => Category::CATEGORY_STATISTICAL, @@ -2644,6 +2702,11 @@ class Calculation 'functionCall' => [LookupRef\VLookup::class, 'lookup'], 'argumentCount' => '3,4', ], + 'VSTACK' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '1+', + ], 'WEBSERVICE' => [ 'category' => Category::CATEGORY_WEB, 'functionCall' => [Web\Service::class, 'webService'], @@ -2679,6 +2742,16 @@ class Calculation 'functionCall' => [Functions::class, 'DUMMY'], 'argumentCount' => '2-4', ], + 'WRAPCOLS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-3', + ], + 'WRAPROWS' => [ + 'category' => Category::CATEGORY_MATH_AND_TRIG, + 'functionCall' => [Functions::class, 'DUMMY'], + 'argumentCount' => '2-3', + ], 'XIRR' => [ 'category' => Category::CATEGORY_FINANCIAL, 'functionCall' => [Financial\CashFlow\Variable\NonPeriodic::class, 'rate'], @@ -2759,11 +2832,10 @@ class Calculation public function __construct(?Spreadsheet $spreadsheet = null) { - $this->delta = 1 * 10 ** (0 - ini_get('precision')); - $this->spreadsheet = $spreadsheet; $this->cyclicReferenceStack = new CyclicReferenceStack(); $this->debugLog = new Logger($this->cyclicReferenceStack); + $this->branchPruner = new BranchPruner($this->branchPruningEnabled); self::$referenceHelper = ReferenceHelper::getInstance(); } @@ -2807,7 +2879,7 @@ class Calculation public function flushInstance(): void { $this->clearCalculationCache(); - $this->clearBranchStore(); + $this->branchPruner->clearBranchStore(); } /** @@ -2959,6 +3031,7 @@ class Calculation public function setBranchPruningEnabled($enabled): void { $this->branchPruningEnabled = $enabled; + $this->branchPruner = new BranchPruner($this->branchPruningEnabled); } public function enableBranchPruning(): void @@ -2971,11 +3044,6 @@ class Calculation $this->setBranchPruningEnabled(false); } - public function clearBranchStore(): void - { - $this->branchStoreKeyCounter = 0; - } - /** * Get the currently defined locale code. * @@ -3020,7 +3088,7 @@ class Calculation } // Test whether we have any language data for this language (any locale) - if (in_array($language, self::$validLocaleLanguages)) { + if (in_array($language, self::$validLocaleLanguages, true)) { // initialise language/locale settings self::$localeFunctions = []; self::$localeArgumentSeparator = ','; @@ -3042,7 +3110,7 @@ class Calculation [$localeFunction] = explode('##', $localeFunction); // Strip out comments if (strpos($localeFunction, '=') !== false) { [$fName, $lfName] = array_map('trim', explode('=', $localeFunction)); - if ((isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { + if ((substr($fName, 0, 1) === '*' || isset(self::$phpSpreadsheetFunctions[$fName])) && ($lfName != '') && ($fName != $lfName)) { self::$localeFunctions[$fName] = $lfName; } } @@ -3089,30 +3157,28 @@ class Calculation return false; } - /** - * @param string $fromSeparator - * @param string $toSeparator - * @param string $formula - * @param bool $inBraces - * - * @return string - */ - public static function translateSeparator($fromSeparator, $toSeparator, $formula, &$inBraces) - { + public static function translateSeparator( + string $fromSeparator, + string $toSeparator, + string $formula, + int &$inBracesLevel, + string $openBrace = self::FORMULA_OPEN_FUNCTION_BRACE, + string $closeBrace = self::FORMULA_CLOSE_FUNCTION_BRACE + ): string { $strlen = mb_strlen($formula); for ($i = 0; $i < $strlen; ++$i) { $chr = mb_substr($formula, $i, 1); switch ($chr) { - case self::FORMULA_OPEN_FUNCTION_BRACE: - $inBraces = true; + case $openBrace: + ++$inBracesLevel; break; - case self::FORMULA_CLOSE_FUNCTION_BRACE: - $inBraces = false; + case $closeBrace: + --$inBracesLevel; break; case $fromSeparator: - if (!$inBraces) { + if ($inBracesLevel > 0) { $formula = mb_substr($formula, 0, $i) . $toSeparator . mb_substr($formula, $i + 1); } } @@ -3121,31 +3187,48 @@ class Calculation return $formula; } - /** - * @param string[] $from - * @param string[] $to - * @param string $formula - * @param string $fromSeparator - * @param string $toSeparator - * - * @return string - */ - private static function translateFormula(array $from, array $to, $formula, $fromSeparator, $toSeparator) + private static function translateFormulaBlock( + array $from, + array $to, + string $formula, + int &$inFunctionBracesLevel, + int &$inMatrixBracesLevel, + string $fromSeparator, + string $toSeparator + ): string { + // Function Names + $formula = (string) preg_replace($from, $to, $formula); + + // Temporarily adjust matrix separators so that they won't be confused with function arguments + $formula = self::translateSeparator(';', '|', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + $formula = self::translateSeparator(',', '!', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + // Function Argument Separators + $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inFunctionBracesLevel); + // Restore matrix separators + $formula = self::translateSeparator('|', ';', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + $formula = self::translateSeparator('!', ',', $formula, $inMatrixBracesLevel, self::FORMULA_OPEN_MATRIX_BRACE, self::FORMULA_CLOSE_MATRIX_BRACE); + + return $formula; + } + + private static function translateFormula(array $from, array $to, string $formula, string $fromSeparator, string $toSeparator): string { - // Convert any Excel function names to the required language + // Convert any Excel function names and constant names to the required language; + // and adjust function argument separators if (self::$localeLanguage !== 'en_us') { - $inBraces = false; - // If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators + $inFunctionBracesLevel = 0; + $inMatrixBracesLevel = 0; + // If there is the possibility of separators within a quoted string, then we treat them as literals if (strpos($formula, self::FORMULA_STRING_QUOTE) !== false) { - // So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded - // the formula + // So instead we skip replacing in any quoted strings by only replacing in every other array element + // after we've exploded the formula $temp = explode(self::FORMULA_STRING_QUOTE, $formula); - $i = false; + $notWithinQuotes = false; foreach ($temp as &$value) { - // Only count/replace in alternating array entries - if ($i = !$i) { - $value = preg_replace($from, $to, $value); - $value = self::translateSeparator($fromSeparator, $toSeparator, $value, $inBraces); + // Only adjust in alternating array entries + $notWithinQuotes = !$notWithinQuotes; + if ($notWithinQuotes === true) { + $value = self::translateFormulaBlock($from, $to, $value, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator); } } unset($value); @@ -3153,8 +3236,7 @@ class Calculation $formula = implode(self::FORMULA_STRING_QUOTE, $temp); } else { // If there's no quoted strings, then we do a simple count/replace - $formula = preg_replace($from, $to, $formula); - $formula = self::translateSeparator($fromSeparator, $toSeparator, $formula, $inBraces); + $formula = self::translateFormulaBlock($from, $to, $formula, $inFunctionBracesLevel, $inMatrixBracesLevel, $fromSeparator, $toSeparator); } } @@ -3167,13 +3249,14 @@ class Calculation public function _translateFormulaToLocale($formula) { + // Build list of function names and constants for translation if (self::$functionReplaceFromExcel === null) { self::$functionReplaceFromExcel = []; foreach (array_keys(self::$localeFunctions) as $excelFunctionName) { - self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/Ui'; + self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelFunctionName, '/') . '([\s]*\()/ui'; } foreach (array_keys(self::$localeBoolean) as $excelBoolean) { - self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui'; + self::$functionReplaceFromExcel[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui'; } } @@ -3187,7 +3270,13 @@ class Calculation } } - return self::translateFormula(self::$functionReplaceFromExcel, self::$functionReplaceToLocale, $formula, ',', self::$localeArgumentSeparator); + return self::translateFormula( + self::$functionReplaceFromExcel, + self::$functionReplaceToLocale, + $formula, + ',', + self::$localeArgumentSeparator + ); } private static $functionReplaceFromLocale; @@ -3199,16 +3288,17 @@ class Calculation if (self::$functionReplaceFromLocale === null) { self::$functionReplaceFromLocale = []; foreach (self::$localeFunctions as $localeFunctionName) { - self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/Ui'; + self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($localeFunctionName, '/') . '([\s]*\()/ui'; } foreach (self::$localeBoolean as $excelBoolean) { - self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/Ui'; + self::$functionReplaceFromLocale[] = '/(@?[^\w\.])' . preg_quote($excelBoolean, '/') . '([^\w\.])/ui'; } } if (self::$functionReplaceToExcel === null) { self::$functionReplaceToExcel = []; foreach (array_keys(self::$localeFunctions) as $excelFunctionName) { + // @phpstan-ignore-next-line self::$functionReplaceToExcel[] = '$1' . trim($excelFunctionName) . '$2'; } foreach (array_keys(self::$localeBoolean) as $excelBoolean) { @@ -3255,7 +3345,7 @@ class Calculation return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE; } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) { // Convert numeric errors to NaN error - return Functions::NAN(); + return Information\ExcelError::NAN(); } return $value; @@ -3276,7 +3366,7 @@ class Calculation } // Convert numeric errors to NAN error } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) { - return Functions::NAN(); + return Information\ExcelError::NAN(); } return $value; @@ -3345,7 +3435,7 @@ class Calculation self::$returnArrayAsType = $returnArrayAsType; $testResult = Functions::flattenArray($result); if (self::$returnArrayAsType == self::RETURN_ARRAY_AS_ERROR) { - return Functions::VALUE(); + return Information\ExcelError::VALUE(); } // If there's only a single cell in the array, then we allow it if (count($testResult) != 1) { @@ -3353,13 +3443,13 @@ class Calculation $r = array_keys($result); $r = array_shift($r); if (!is_numeric($r)) { - return Functions::VALUE(); + return Information\ExcelError::VALUE(); } if (is_array($result[$r])) { $c = array_keys($result[$r]); $c = array_shift($c); if (!is_numeric($c)) { - return Functions::VALUE(); + return Information\ExcelError::VALUE(); } } } @@ -3370,7 +3460,7 @@ class Calculation if ($result === null && $cell->getWorksheet()->getSheetView()->getShowZeros()) { return 0; } elseif ((is_float($result)) && ((is_nan($result)) || (is_infinite($result)))) { - return Functions::NAN(); + return Information\ExcelError::NAN(); } return $result; @@ -3446,11 +3536,11 @@ class Calculation */ public function getValueFromCache(string $cellReference, &$cellValue): bool { - $this->debugLog->writeDebugLog("Testing cache value for cell {$cellReference}"); + $this->debugLog->writeDebugLog('Testing cache value for cell %s', $cellReference); // Is calculation cacheing enabled? // If so, is the required value present in calculation cache? if (($this->calculationCacheEnabled) && (isset($this->calculationCache[$cellReference]))) { - $this->debugLog->writeDebugLog("Retrieving value for cell {$cellReference} from cache"); + $this->debugLog->writeDebugLog('Retrieving value for cell %s from cache', $cellReference); // Return the cached result $cellValue = $this->calculationCache[$cellReference]; @@ -3512,7 +3602,7 @@ class Calculation if (($cellID !== null) && ($this->getValueFromCache($wsCellReference, $cellValue))) { return $cellValue; } - $this->debugLog->writeDebugLog("Evaluating formula for cell {$wsCellReference}"); + $this->debugLog->writeDebugLog('Evaluating formula for cell %s', $wsCellReference); if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) { if ($this->cyclicFormulaCount <= 0) { @@ -3534,7 +3624,7 @@ class Calculation } } - $this->debugLog->writeDebugLog("Formula for cell {$wsCellReference} is {$formula}"); + $this->debugLog->writeDebugLog('Formula for cell %s is %s', $wsCellReference, $formula); // Parse the formula onto the token stack and calculate the value $this->cyclicReferenceStack->push($wsCellReference); @@ -3742,6 +3832,8 @@ class Calculation return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE; } elseif (is_bool($value)) { return ($value) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE']; + } elseif ($value === null) { + return self::$localeBoolean['NULL']; } } @@ -3795,11 +3887,11 @@ class Calculation */ private function convertMatrixReferences($formula) { - static $matrixReplaceFrom = [self::FORMULA_OPEN_FUNCTION_BRACE, ';', self::FORMULA_CLOSE_FUNCTION_BRACE]; + static $matrixReplaceFrom = [self::FORMULA_OPEN_MATRIX_BRACE, ';', self::FORMULA_CLOSE_MATRIX_BRACE]; static $matrixReplaceTo = ['MKMATRIX(MKMATRIX(', '),MKMATRIX(', '))']; // Convert any Excel matrix references to the MKMATRIX() function - if (strpos($formula, self::FORMULA_OPEN_FUNCTION_BRACE) !== false) { + if (strpos($formula, self::FORMULA_OPEN_MATRIX_BRACE) !== false) { // If there is the possibility of braces within a quoted string, then we don't treat those as matrix indicators if (strpos($formula, self::FORMULA_STRING_QUOTE) !== false) { // So instead we skip replacing in any quoted strings by only replacing in every other array element after we've exploded @@ -3807,12 +3899,13 @@ class Calculation $temp = explode(self::FORMULA_STRING_QUOTE, $formula); // Open and Closed counts used for trapping mismatched braces in the formula $openCount = $closeCount = 0; - $i = false; + $notWithinQuotes = false; foreach ($temp as &$value) { // Only count/replace in alternating array entries - if ($i = !$i) { - $openCount += substr_count($value, self::FORMULA_OPEN_FUNCTION_BRACE); - $closeCount += substr_count($value, self::FORMULA_CLOSE_FUNCTION_BRACE); + $notWithinQuotes = !$notWithinQuotes; + if ($notWithinQuotes === true) { + $openCount += substr_count($value, self::FORMULA_OPEN_MATRIX_BRACE); + $closeCount += substr_count($value, self::FORMULA_CLOSE_MATRIX_BRACE); $value = str_replace($matrixReplaceFrom, $matrixReplaceTo, $value); } } @@ -3821,8 +3914,8 @@ class Calculation $formula = implode(self::FORMULA_STRING_QUOTE, $temp); } else { // If there's no quoted strings, then we do a simple count/replace - $openCount = substr_count($formula, self::FORMULA_OPEN_FUNCTION_BRACE); - $closeCount = substr_count($formula, self::FORMULA_CLOSE_FUNCTION_BRACE); + $openCount = substr_count($formula, self::FORMULA_OPEN_MATRIX_BRACE); + $closeCount = substr_count($formula, self::FORMULA_CLOSE_MATRIX_BRACE); $formula = str_replace($matrixReplaceFrom, $matrixReplaceTo, $formula); } // Trap for mismatched braces and trigger an appropriate error @@ -3852,7 +3945,7 @@ class Calculation '*' => 0, '/' => 0, // Multiplication and Division '+' => 0, '-' => 0, // Addition and Subtraction '&' => 0, // Concatenation - '|' => 0, ':' => 0, // Intersect and Range + '∪' => 0, '∩' => 0, ':' => 0, // Union, Intersect and Range '>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison ]; @@ -3864,8 +3957,9 @@ class Calculation // This list includes all valid operators, whether binary (including boolean) or unary (such as %) // Array key is the operator, the value is its precedence private static $operatorPrecedence = [ - ':' => 8, // Range - '|' => 7, // Intersect + ':' => 9, // Range + '∩' => 8, // Intersect + '∪' => 7, // Union '~' => 6, // Negation '%' => 5, // Percentage '^' => 4, // Exponentiation @@ -3892,12 +3986,12 @@ class Calculation // so we store the parent worksheet so that we can re-attach it when necessary $pCellParent = ($cell !== null) ? $cell->getWorksheet() : null; - $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION . + $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_STRING . + '|' . self::CALCULATION_REGEXP_FUNCTION . '|' . self::CALCULATION_REGEXP_CELLREF . - '|' . self::CALCULATION_REGEXP_COLUMN_RANGE . - '|' . self::CALCULATION_REGEXP_ROW_RANGE . + '|' . self::CALCULATION_REGEXP_COLUMN_RANGE . + '|' . self::CALCULATION_REGEXP_ROW_RANGE . '|' . self::CALCULATION_REGEXP_NUMBER . - '|' . self::CALCULATION_REGEXP_STRING . '|' . self::CALCULATION_REGEXP_OPENBRACE . '|' . self::CALCULATION_REGEXP_DEFINEDNAME . '|' . self::CALCULATION_REGEXP_ERROR . @@ -3905,58 +3999,19 @@ class Calculation // Start with initialisation $index = 0; - $stack = new Stack(); + $stack = new Stack($this->branchPruner); $output = []; $expectingOperator = false; // We use this test in syntax-checking the expression to determine when a // - 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; - } - } - } + $this->branchPruner->initialiseForLoop(); $opCharacter = $formula[$index]; // Get the first character of the value at the current index position @@ -3966,17 +4021,18 @@ class Calculation // Find out if we're currently at the beginning of a number, variable, cell reference, function, parenthesis or operand $isOperandOrFunction = (bool) preg_match($regexpMatchString, substr($formula, $index), $match); + $expectingOperatorCopy = $expectingOperator; if ($opCharacter == '-' && !$expectingOperator) { // Is it a negation instead of a minus? // Put a negation on the stack - $stack->push('Unary Operator', '~', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $stack->push('Unary Operator', '~'); ++$index; // and drop the negation symbol } elseif ($opCharacter == '%' && $expectingOperator) { // Put a percentage on the stack - $stack->push('Unary Operator', '%', null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $stack->push('Unary Operator', '%'); ++$index; } elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded? ++$index; // Drop the redundant plus symbol - } elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) { // We have to explicitly deny a tilde or pipe, because they are legal + } elseif ((($opCharacter == '~') || ($opCharacter == '∩') || ($opCharacter == '∪')) && (!$isOperandOrFunction)) { // We have to explicitly deny a tilde, union or intersect because they are legal return $this->raiseFormulaError("Formula Error: Illegal character '~'"); // on the stack but not in the input expression } elseif ((isset(self::$operators[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) { // Are we putting an operator on the stack? while ( @@ -3989,13 +4045,13 @@ class Calculation } // Finally put our current operator onto the stack - $stack->push('Binary Operator', $opCharacter, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $stack->push('Binary Operator', $opCharacter); ++$index; $expectingOperator = false; - } elseif ($opCharacter == ')' && $expectingOperator) { // Are we expecting to close a parenthesis? + } elseif ($opCharacter == ')' && $expectingOperator) { // Are we expecting to close a parenthesis? $expectingOperand = false; - 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) { return $this->raiseFormulaError('Formula Error: Unexpected closing brace ")"'); } @@ -4005,29 +4061,19 @@ class Calculation // Branch pruning we decrease the depth whether is it a function // call or a parenthesis - if (!empty($pendingStoreKey)) { - --$parenthesisDepthMap[$pendingStoreKey]; - } + $this->branchPruner->decrementDepth(); - if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $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]; - array_pop($pendingStoreKeysStack); - unset($pendingStoreKey); + if (is_array($d) && preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) { + // Did this parenthesis just close a function? + try { + $this->branchPruner->closingBrace($d['value']); + } catch (Exception $e) { + return $this->raiseFormulaError($e->getMessage()); } $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) + $argumentCount = $d['value'] ?? 0; // See how many arguments there were (argument count is the next value stored on the stack) $output[] = $d; // Dump the argument count on the output $output[] = $stack->pop(); // Pop the function and push onto the output if (isset(self::$controlFunctions[$functionName])) { @@ -4085,23 +4131,14 @@ 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'); - } + } elseif ($opCharacter == ',') { // Is this the separator for function arguments? + try { + $this->branchPruner->argumentSeparator(); + } catch (Exception $e) { + return $this->raiseFormulaError($e->getMessage()); } - 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) { return $this->raiseFormulaError('Formula Error: Unexpected ,'); } @@ -4110,36 +4147,41 @@ class Calculation // If we've a comma when we're expecting an operand, then what we actually have is a null operand; // so push a null onto the stack if (($expectingOperand) || (!$expectingOperator)) { - $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => null]; + $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => 'NULL']; } // make sure there was a function $d = $stack->last(2); - if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'], $matches)) { + if (!preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $d['value'] ?? '', $matches)) { + // Can we inject a dummy function at this point so that the braces at least have some context + // because at least the braces are paired up (at this stage in the formula) + // MS Excel allows this if the content is cell references; but doesn't allow actual values, + // but at this point, we can't differentiate (so allow both) return $this->raiseFormulaError('Formula Error: Unexpected ,'); } + + /** @var array $d */ $d = $stack->pop(); - $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 + ++$d['value']; // increment the argument count + + $stack->pushStackItem($d); + $stack->push('Brace', '('); // put the ( back on, we'll need to pop back to it again + $expectingOperator = false; $expectingOperand = true; ++$index; } elseif ($opCharacter == '(' && !$expectingOperator) { - if (!empty($pendingStoreKey)) { // Branch pruning: we go deeper - ++$parenthesisDepthMap[$pendingStoreKey]; - } - $stack->push('Brace', '(', null, $currentCondition, $currentOnlyIf, $currentOnlyIf); + // Branch pruning: we go deeper + $this->branchPruner->incrementDepth(); + $stack->push('Brace', '(', null); ++$index; - } elseif ($isOperandOrFunction && !$expectingOperator) { // do we now have a function/variable/number? + } elseif ($isOperandOrFunction && !$expectingOperatorCopy) { // do we now have a function/variable/number? $expectingOperator = true; $expectingOperand = false; $val = $match[1]; $length = strlen($val); if (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $val, $matches)) { - $val = preg_replace('/\s/u', '', $val); + $val = (string) preg_replace('/\s/u', '', $val); if (isset(self::$phpSpreadsheetFunctions[strtoupper($matches[1])]) || isset(self::$controlFunctions[strtoupper($matches[1])])) { // it's a function $valToUpper = strtoupper($val); } else { @@ -4147,46 +4189,46 @@ class Calculation } // 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 an if but we go deeper - if (!empty($pendingStoreKey) && array_key_exists($pendingStoreKey, $parenthesisDepthMap)) { - ++$parenthesisDepthMap[$pendingStoreKey]; - } - } - $stack->push('Function', $valToUpper, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $this->branchPruner->functionCall($valToUpper); + + $stack->push('Function', $valToUpper); // tests if the function is closed right after opening $ax = preg_match('/^\s*\)/u', substr($formula, $index + $length)); if ($ax) { - $stack->push('Operand Count for Function ' . $valToUpper . ')', 0, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $stack->push('Operand Count for Function ' . $valToUpper . ')', 0); $expectingOperator = true; } else { - $stack->push('Operand Count for Function ' . $valToUpper . ')', 1, null, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $stack->push('Operand Count for Function ' . $valToUpper . ')', 1); $expectingOperator = false; } $stack->push('Brace', '('); - } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $val, $matches)) { + } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $val, $matches)) { // Watch for this case-change when modifying to allow cell references in different worksheets... // Should only be applied to the actual cell column, not the worksheet name // If the last entry on the stack was a : operator, then we have a cell range reference $testPrevOp = $stack->last(1); if ($testPrevOp !== null && $testPrevOp['value'] === ':') { // If we have a worksheet reference, then we're playing with a 3D reference - if ($matches[2] == '') { + if ($matches[2] === '') { // Otherwise, we 'inherit' the worksheet reference from the start cell reference // The start of the cell range reference should be the last entry in $output $rangeStartCellRef = $output[count($output) - 1]['value']; - preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches); + if ($rangeStartCellRef === ':') { + // Do we have chained range operators? + $rangeStartCellRef = $output[count($output) - 2]['value']; + } + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches); if ($rangeStartMatches[2] > '') { $val = $rangeStartMatches[2] . '!' . $val; } } else { $rangeStartCellRef = $output[count($output) - 1]['value']; - preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $rangeStartCellRef, $rangeStartMatches); + if ($rangeStartCellRef === ':') { + // Do we have chained range operators? + $rangeStartCellRef = $output[count($output) - 2]['value']; + } + preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/miu', $rangeStartCellRef, $rangeStartMatches); if ($rangeStartMatches[2] !== $matches[2]) { return $this->raiseFormulaError('3D Range references are not yet supported'); } @@ -4195,11 +4237,12 @@ class Calculation $worksheet = $pCellParent->getTitle(); $val = "'{$worksheet}'!{$val}"; } - - $outputItem = $stack->getStackItem('Cell Reference', $val, $val, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); + $outputItem = $stack->getStackItem('Cell Reference', $val, $val); $output[] = $outputItem; - } else { // it's a variable, constant, string, number or boolean + } else { // it's a variable, constant, string, number or boolean $localeConstant = false; $stackItemType = 'Value'; $stackItemReference = null; @@ -4208,39 +4251,65 @@ class Calculation $testPrevOp = $stack->last(1); if ($testPrevOp !== null && $testPrevOp['value'] === ':') { $stackItemType = 'Cell Reference'; - $startRowColRef = $output[count($output) - 1]['value']; - [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true); - $rangeSheetRef = $rangeWS1; - if ($rangeWS1 !== '') { - $rangeWS1 .= '!'; - } - $rangeSheetRef = trim($rangeSheetRef, "'"); - [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); - if ($rangeWS2 !== '') { - $rangeWS2 .= '!'; + + if ( + !is_numeric($val) && + ((ctype_alpha($val) === false || strlen($val) > 3)) && + (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $val) !== false) && + ($this->spreadsheet->getNamedRange($val) !== null) + ) { + $namedRange = $this->spreadsheet->getNamedRange($val); + if ($namedRange !== null) { + $stackItemType = 'Defined Name'; + $address = str_replace('$', '', $namedRange->getValue()); + $stackItemReference = $val; + if (strpos($address, ':') !== false) { + // We'll need to manipulate the stack for an actual named range rather than a named cell + $fromTo = explode(':', $address); + $to = array_pop($fromTo); + foreach ($fromTo as $from) { + $output[] = $stack->getStackItem($stackItemType, $from, $stackItemReference); + $output[] = $stack->getStackItem('Binary Operator', ':'); + } + $address = $to; + } + $val = $address; + } } else { - $rangeWS2 = $rangeWS1; - } + $startRowColRef = $output[count($output) - 1]['value']; + [$rangeWS1, $startRowColRef] = Worksheet::extractSheetTitle($startRowColRef, true); + $rangeSheetRef = $rangeWS1; + if ($rangeWS1 !== '') { + $rangeWS1 .= '!'; + } + $rangeSheetRef = trim($rangeSheetRef, "'"); + [$rangeWS2, $val] = Worksheet::extractSheetTitle($val, true); + if ($rangeWS2 !== '') { + $rangeWS2 .= '!'; + } else { + $rangeWS2 = $rangeWS1; + } - $refSheet = $pCellParent; - if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) { - $refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef); - } + $refSheet = $pCellParent; + if ($pCellParent !== null && $rangeSheetRef !== '' && $rangeSheetRef !== $pCellParent->getTitle()) { + $refSheet = $pCellParent->getParent()->getSheetByName($rangeSheetRef); + } - if (ctype_digit($val) && $val <= 1048576) { - // Row range - $stackItemType = 'Row Reference'; - /** @var int $valx */ - $valx = $val; - $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007 - $val = "{$rangeWS2}{$endRowColRef}{$val}"; - } elseif (ctype_alpha($val) && strlen($val) <= 3) { - // Column range - $stackItemType = 'Column Reference'; - $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007 - $val = "{$rangeWS2}{$val}{$endRowColRef}"; + if (ctype_digit($val) && $val <= 1048576) { + // Row range + $stackItemType = 'Row Reference'; + /** @var int $valx */ + $valx = $val; + $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataColumn($valx) : 'XFD'; // Max 16,384 columns for Excel2007 + $val = "{$rangeWS2}{$endRowColRef}{$val}"; + } elseif (ctype_alpha($val) && strlen($val) <= 3) { + // Column range + $stackItemType = 'Column Reference'; + $endRowColRef = ($refSheet !== null) ? $refSheet->getHighestDataRow($val) : 1048576; // Max 1,048,576 rows for Excel2007 + $val = "{$rangeWS2}{$val}{$endRowColRef}"; + } + $stackItemReference = $val; } - $stackItemReference = $val; } elseif ($opCharacter == self::FORMULA_STRING_QUOTE) { // UnEscape any quotes within the string $val = self::wrapResult(str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($val))); @@ -4257,6 +4326,8 @@ class Calculation $val = $rowRangeReference[1]; $length = strlen($rowRangeReference[1]); $stackItemType = 'Row Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); $column = 'A'; if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { $column = $pCellParent->getHighestDataColumn($val); @@ -4269,6 +4340,8 @@ class Calculation $val = $columnRangeReference[1]; $length = strlen($val); $stackItemType = 'Column Reference'; + // unescape any apostrophes or double quotes in worksheet name + $val = str_replace(["''", '""'], ["'", '"'], $val); $row = '1'; if (($testPrevOp !== null && $testPrevOp['value'] === ':') && $pCellParent !== null) { $row = $pCellParent->getHighestDataRow($val); @@ -4286,18 +4359,18 @@ class Calculation } } - $details = $stack->getStackItem($stackItemType, $val, $stackItemReference, $currentCondition, $currentOnlyIf, $currentOnlyIfNot); + $details = $stack->getStackItem($stackItemType, $val, $stackItemReference); if ($localeConstant) { $details['localeValue'] = $localeConstant; } $output[] = $details; } $index += $length; - } elseif ($opCharacter == '$') { // absolute row or column range + } elseif ($opCharacter == '$') { // absolute row or column range ++$index; - } elseif ($opCharacter == ')') { // miscellaneous error checking + } elseif ($opCharacter == ')') { // miscellaneous error checking if ($expectingOperand) { - $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => null]; + $output[] = ['type' => 'Empty Argument', 'value' => self::$excelConstants['NULL'], 'reference' => 'NULL']; $expectingOperand = false; $expectingOperator = true; } else { @@ -4324,19 +4397,23 @@ class Calculation } if ($formula[$index] == ' ') { - while ($formula[$index] == ' ') { + while ($formula[$index] === ' ') { ++$index; } // If we're expecting an operator, but only have a space between the previous and next operands (and both are // Cell References) then we have an INTERSECTION operator + $countOutputMinus1 = count($output) - 1; if ( ($expectingOperator) && + array_key_exists($countOutputMinus1, $output) && + is_array($output[$countOutputMinus1]) && + array_key_exists('type', $output[$countOutputMinus1]) && ( (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) && - ($output[count($output) - 1]['type'] == 'Cell Reference') || + ($output[$countOutputMinus1]['type'] === 'Cell Reference') || (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '.*/miu', substr($formula, $index), $match)) && - ($output[count($output) - 1]['type'] == 'Defined Name' || $output[count($output) - 1]['type'] == 'Value') + ($output[$countOutputMinus1]['type'] === 'Defined Name' || $output[$countOutputMinus1]['type'] === 'Value') ) ) { while ( @@ -4347,13 +4424,14 @@ class Calculation ) { $output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output } - $stack->push('Binary Operator', '|'); // Put an Intersect Operator on the stack + $stack->push('Binary Operator', '∩'); // Put an Intersect Operator on the stack $expectingOperator = false; } } } - while (($op = $stack->pop()) !== null) { // pop everything off the stack and push onto output + while (($op = $stack->pop()) !== null) { + // pop everything off the stack and push onto output if ((is_array($op) && $op['value'] == '(') || ($op === '(')) { return $this->raiseFormulaError("Formula Error: Expecting ')'"); // if there are any opening braces on the stack, then braces were unbalanced } @@ -4369,6 +4447,12 @@ class Calculation if (($operandData['reference'] === null) && (is_array($operand))) { $rKeys = array_keys($operand); $rowKey = array_shift($rKeys); + if (is_array($operand[$rowKey]) === false) { + $operandData['value'] = $operand[$rowKey]; + + return $operand[$rowKey]; + } + $cKeys = array_keys(array_keys($operand[$rowKey])); $colKey = array_shift($cKeys); if (ctype_upper("$colKey")) { @@ -4389,7 +4473,7 @@ class Calculation */ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null) { - if ($tokens == false) { + if ($tokens === false) { return false; } @@ -4397,7 +4481,7 @@ class Calculation // so we store the parent cell collection so that we can re-attach it when necessary $pCellWorksheet = ($cell !== null) ? $cell->getWorksheet() : null; $pCellParent = ($cell !== null) ? $cell->getParent() : null; - $stack = new Stack(); + $stack = new Stack($this->branchPruner); // Stores branches that have been pruned $fakedForBranchPruning = []; @@ -4406,7 +4490,6 @@ class Calculation // 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'])) { @@ -4416,16 +4499,12 @@ class Calculation true : (bool) Functions::flattenSingleValue($storeValue); if (is_array($storeValue)) { $wrappedItem = end($storeValue); - $storeValue = end($wrappedItem); + $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem; } if ( - isset($storeValue) - && ( - !$storeValueAsBool - || Functions::isError($storeValue) - || ($storeValue === 'Pruned branch') - ) + (isset($storeValue) || $tokenData['reference'] === 'NULL') + && (!$storeValueAsBool || ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) ) { // If branching value is not true, we don't need to compute if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) { @@ -4452,15 +4531,12 @@ class Calculation true : (bool) Functions::flattenSingleValue($storeValue); if (is_array($storeValue)) { $wrappedItem = end($storeValue); - $storeValue = end($wrappedItem); + $storeValue = is_array($wrappedItem) ? end($wrappedItem) : $wrappedItem; } + if ( - isset($storeValue) - && ( - $storeValueAsBool - || Functions::isError($storeValue) - || ($storeValue === 'Pruned branch') - ) + (isset($storeValue) || $tokenData['reference'] === 'NULL') + && ($storeValueAsBool || ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) ) { // If branching value is true, we don't need to compute if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) { @@ -4495,32 +4571,40 @@ class Calculation // Log what we're doing if ($token == ':') { - $this->debugLog->writeDebugLog('Evaluating Range ', $this->showValue($operand1Data['reference']), ' ', $token, ' ', $this->showValue($operand2Data['reference'])); + $this->debugLog->writeDebugLog('Evaluating Range %s %s %s', $this->showValue($operand1Data['reference']), $token, $this->showValue($operand2Data['reference'])); } else { - $this->debugLog->writeDebugLog('Evaluating ', $this->showValue($operand1), ' ', $token, ' ', $this->showValue($operand2)); + $this->debugLog->writeDebugLog('Evaluating %s %s %s', $this->showValue($operand1), $token, $this->showValue($operand2)); } // Process the operation in the appropriate manner switch ($token) { - // Comparison (Boolean) Operators - case '>': // Greater than - case '<': // Less than - case '>=': // Greater than or Equal to - case '<=': // Less than or Equal to - case '=': // Equality - case '<>': // Inequality - $result = $this->executeBinaryComparisonOperation($cellID, $operand1, $operand2, $token, $stack); + // Comparison (Boolean) Operators + case '>': // Greater than + case '<': // Less than + case '>=': // Greater than or Equal to + case '<=': // Less than or Equal to + case '=': // Equality + case '<>': // Inequality + $result = $this->executeBinaryComparisonOperation($operand1, $operand2, (string) $token, $stack); if (isset($storeKey)) { $branchStore[$storeKey] = $result; } break; - // Binary Operators - case ':': // Range + // Binary Operators + case ':': // Range + if ($operand1Data['type'] === 'Defined Name') { + if (preg_match('/$' . self::CALCULATION_REGEXP_DEFINEDNAME . '^/mui', $operand1Data['reference']) !== false) { + $definedName = $this->spreadsheet->getNamedRange($operand1Data['reference']); + if ($definedName !== null) { + $operand1Data['reference'] = $operand1Data['value'] = str_replace('$', '', $definedName->getValue()); + } + } + } if (strpos($operand1Data['reference'], '!') !== false) { [$sheet1, $operand1Data['reference']] = Worksheet::extractSheetTitle($operand1Data['reference'], true); } else { - $sheet1 = ($pCellParent !== null) ? $pCellWorksheet->getTitle() : ''; + $sheet1 = ($pCellWorksheet !== null) ? $pCellWorksheet->getTitle() : ''; } [$sheet2, $operand2Data['reference']] = Worksheet::extractSheetTitle($operand2Data['reference'], true); @@ -4529,19 +4613,23 @@ class Calculation } if (trim($sheet1, "'") === trim($sheet2, "'")) { - if ($operand1Data['reference'] === null) { - if ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) { + if ($operand1Data['reference'] === null && $cell !== null) { + if (is_array($operand1Data['value'])) { + $operand1Data['reference'] = $cell->getCoordinate(); + } elseif ((trim($operand1Data['value']) != '') && (is_numeric($operand1Data['value']))) { $operand1Data['reference'] = $cell->getColumn() . $operand1Data['value']; - } elseif (trim($operand1Data['reference']) == '') { + } elseif (trim($operand1Data['value']) == '') { $operand1Data['reference'] = $cell->getCoordinate(); } else { $operand1Data['reference'] = $operand1Data['value'] . $cell->getRow(); } } - if ($operand2Data['reference'] === null) { - if ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) { + if ($operand2Data['reference'] === null && $cell !== null) { + if (is_array($operand2Data['value'])) { + $operand2Data['reference'] = $cell->getCoordinate(); + } elseif ((trim($operand2Data['value']) != '') && (is_numeric($operand2Data['value']))) { $operand2Data['reference'] = $cell->getColumn() . $operand2Data['value']; - } elseif (trim($operand2Data['reference']) == '') { + } elseif (trim($operand2Data['value']) == '') { $operand2Data['reference'] = $cell->getCoordinate(); } else { $operand2Data['reference'] = $operand2Data['value'] . $cell->getRow(); @@ -4564,7 +4652,7 @@ class Calculation $stack->push('Cell Reference', $cellValue, $cellRef); } else { - $stack->push('Error', Functions::REF(), null); + $stack->push('Error', Information\ExcelError::REF(), null); } break; @@ -4623,14 +4711,22 @@ class Calculation // Perform the required operation against the operand 1 matrix, passing in operand 2 $matrixResult = $matrix->concat($operand2); $result = $matrixResult->getArray(); + if (isset($result[0][0])) { + $result[0][0] = Shared\StringHelper::substring($result[0][0], 0, DataType::MAX_STRING_LENGTH); + } } catch (\Exception $ex) { - $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage()); + $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); $result = '#VALUE!'; } } else { + // In theory, we should truncate here. + // But I can't figure out a formula + // using the concatenation operator + // with literals that fits in 32K, + // so I don't think we can overflow here. $result = self::FORMULA_STRING_QUOTE . str_replace('""', self::FORMULA_STRING_QUOTE, self::unwrapResult($operand1) . self::unwrapResult($operand2)) . self::FORMULA_STRING_QUOTE; } - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); $stack->push('Value', $result); if (isset($storeKey)) { @@ -4638,7 +4734,7 @@ class Calculation } break; - case '|': // Intersect + case '∩': // Intersect $rowIntersect = array_intersect_key($operand1, $operand2); $cellIntersect = $oCol = $oRow = []; foreach (array_keys($rowIntersect) as $row) { @@ -4649,29 +4745,28 @@ class Calculation } } if (count(Functions::flattenArray($cellIntersect)) === 0) { - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect)); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect)); $stack->push('Error', Functions::null(), null); } else { $cellRef = Coordinate::stringFromColumnIndex(min($oCol) + 1) . min($oRow) . ':' . Coordinate::stringFromColumnIndex(max($oCol) + 1) . max($oRow); - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($cellIntersect)); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($cellIntersect)); $stack->push('Value', $cellIntersect, $cellRef); } break; } - - // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on } elseif (($token === '~') || ($token === '%')) { + // if the token is a unary operator, pop one value off the stack, do the operation, and push it back on if (($arg = $stack->pop()) === null) { return $this->raiseFormulaError('Internal error - Operand value missing from stack'); } $arg = $arg['value']; if ($token === '~') { - $this->debugLog->writeDebugLog('Evaluating Negation of ', $this->showValue($arg)); + $this->debugLog->writeDebugLog('Evaluating Negation of %s', $this->showValue($arg)); $multiplier = -1; } else { - $this->debugLog->writeDebugLog('Evaluating Percentile of ', $this->showValue($arg)); + $this->debugLog->writeDebugLog('Evaluating Percentile of %s', $this->showValue($arg)); $multiplier = 0.01; } if (is_array($arg)) { @@ -4682,10 +4777,10 @@ class Calculation $matrixResult = $matrix1->arrayTimesEquals($multiplier); $result = $matrixResult->getArray(); } catch (\Exception $ex) { - $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage()); + $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); $result = '#VALUE!'; } - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); $stack->push('Value', $result); if (isset($storeKey)) { $branchStore[$storeKey] = $result; @@ -4698,8 +4793,8 @@ class Calculation if (isset($matches[8])) { if ($cell === null) { - // We can't access the range, so return a REF error - $cellValue = Functions::REF(); + // We can't access the range, so return a REF error + $cellValue = Information\ExcelError::REF(); } else { $cellRef = $matches[6] . $matches[7] . ':' . $matches[9] . $matches[10]; if ($matches[2] > '') { @@ -4709,27 +4804,27 @@ class Calculation return $this->raiseFormulaError('Unable to access External Workbook'); } $matches[2] = trim($matches[2], "\"'"); - $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in worksheet ', $matches[2]); + $this->debugLog->writeDebugLog('Evaluating Cell Range %s in worksheet %s', $cellRef, $matches[2]); if ($pCellParent !== null) { $cellValue = $this->extractCellRange($cellRef, $this->spreadsheet->getSheetByName($matches[2]), false); } else { return $this->raiseFormulaError('Unable to access Cell Reference'); } - $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue)); + $this->debugLog->writeDebugLog('Evaluation Result for cells %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue)); } else { - $this->debugLog->writeDebugLog('Evaluating Cell Range ', $cellRef, ' in current worksheet'); + $this->debugLog->writeDebugLog('Evaluating Cell Range %s in current worksheet', $cellRef); if ($pCellParent !== null) { $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false); } else { return $this->raiseFormulaError('Unable to access Cell Reference'); } - $this->debugLog->writeDebugLog('Evaluation Result for cells ', $cellRef, ' is ', $this->showTypeDetails($cellValue)); + $this->debugLog->writeDebugLog('Evaluation Result for cells %s is %s', $cellRef, $this->showTypeDetails($cellValue)); } } } else { if ($cell === null) { // We can't access the cell, so return a REF error - $cellValue = Functions::REF(); + $cellValue = Information\ExcelError::REF(); } else { $cellRef = $matches[6] . $matches[7]; if ($matches[2] > '') { @@ -4738,7 +4833,7 @@ class Calculation // It's a Reference to an external spreadsheet (not currently supported) return $this->raiseFormulaError('Unable to access External Workbook'); } - $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in worksheet ', $matches[2]); + $this->debugLog->writeDebugLog('Evaluating Cell %s in worksheet %s', $cellRef, $matches[2]); if ($pCellParent !== null) { $cellSheet = $this->spreadsheet->getSheetByName($matches[2]); if ($cellSheet && $cellSheet->cellExists($cellRef)) { @@ -4746,21 +4841,21 @@ class Calculation $cell->attach($pCellParent); } else { $cellRef = ($cellSheet !== null) ? "'{$matches[2]}'!{$cellRef}" : $cellRef; - $cellValue = null; + $cellValue = ($cellSheet !== null) ? null : Information\ExcelError::REF(); } } else { return $this->raiseFormulaError('Unable to access Cell Reference'); } - $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' in worksheet ', $matches[2], ' is ', $this->showTypeDetails($cellValue)); + $this->debugLog->writeDebugLog('Evaluation Result for cell %s in worksheet %s is %s', $cellRef, $matches[2], $this->showTypeDetails($cellValue)); } else { - $this->debugLog->writeDebugLog('Evaluating Cell ', $cellRef, ' in current worksheet'); - if ($pCellParent->has($cellRef)) { + $this->debugLog->writeDebugLog('Evaluating Cell %s in current worksheet', $cellRef); + if ($pCellParent !== null && $pCellParent->has($cellRef)) { $cellValue = $this->extractCellRange($cellRef, $pCellWorksheet, false); $cell->attach($pCellParent); } else { $cellValue = null; } - $this->debugLog->writeDebugLog('Evaluation Result for cell ', $cellRef, ' is ', $this->showTypeDetails($cellValue)); + $this->debugLog->writeDebugLog('Evaluation Result for cell %s is %s', $cellRef, $this->showTypeDetails($cellValue)); } } } @@ -4769,9 +4864,8 @@ class Calculation if (isset($storeKey)) { $branchStore[$storeKey] = $cellValue; } - - // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on } elseif (preg_match('/^' . self::CALCULATION_REGEXP_FUNCTION . '$/miu', $token ?? '', $matches)) { + // if the token is a function, pop arguments off the stack, hand them to the function, and push the result back on if ($pCellParent) { $cell->attach($pCellParent); } @@ -4780,7 +4874,7 @@ class Calculation $argCount = $stack->pop(); $argCount = $argCount['value']; if ($functionName !== 'MKMATRIX') { - $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's')); + $this->debugLog->writeDebugLog('Evaluating Function %s() with %s argument%s', self::localeFunc($functionName), (($argCount == 0) ? 'no' : $argCount), (($argCount == 1) ? '' : 's')); } if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) { // function $passByReference = false; @@ -4843,7 +4937,7 @@ class Calculation if ($functionName !== 'MKMATRIX') { if ($this->debugLog->getWriteDebugLog()) { krsort($argArrayVals); - $this->debugLog->writeDebugLog('Evaluating ', self::localeFunc($functionName), '( ', implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals)), ' )'); + $this->debugLog->writeDebugLog('Evaluating %s ( %s )', self::localeFunc($functionName), implode(self::$localeArgumentSeparator . ' ', Functions::flattenArray($argArrayVals))); } } @@ -4860,7 +4954,7 @@ class Calculation $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)); + $this->debugLog->writeDebugLog('Evaluation Result for %s() function call is %s', self::localeFunc($functionName), $this->showTypeDetails($result)); } $stack->push('Value', self::wrapResult($result)); if (isset($storeKey)) { @@ -4875,20 +4969,20 @@ class Calculation 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 %s as %s', $excelConstant, $this->showTypeDetails(self::$excelConstants[$excelConstant])); } elseif ((is_numeric($token)) || ($token === null) || (is_bool($token)) || ($token == '') || ($token[0] == self::FORMULA_STRING_QUOTE) || ($token[0] == '#')) { $stack->push($tokenData['type'], $token, $tokenData['reference']); if (isset($storeKey)) { $branchStore[$storeKey] = $token; } - // if the token is a named range or formula, evaluate it and push the result onto the stack } elseif (preg_match('/^' . self::CALCULATION_REGEXP_DEFINEDNAME . '$/miu', $token, $matches)) { + // if the token is a named range or formula, evaluate it and push the result onto the stack $definedName = $matches[6]; if ($cell === null || $pCellWorksheet === null) { return $this->raiseFormulaError("undefined name '$token'"); } - $this->debugLog->writeDebugLog('Evaluating Defined Name ', $definedName); + $this->debugLog->writeDebugLog('Evaluating Defined Name %s', $definedName); $namedRange = DefinedName::resolveName($definedName, $pCellWorksheet); if ($namedRange === null) { return $this->raiseFormulaError("undefined name '$definedName'"); @@ -4934,13 +5028,13 @@ class Calculation // If not a numeric, test to see if the value is an Excel error, and so can't be used in normal binary operations if ($operand > '' && $operand[0] == '#') { $stack->push('Value', $operand); - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($operand)); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($operand)); return false; } elseif (!Shared\StringHelper::convertToNumberIfFraction($operand)) { // If not a numeric or a fraction, then it's a text string, and so can't be used in mathematical binary operations $stack->push('Error', '#VALUE!'); - $this->debugLog->writeDebugLog('Evaluation Result is a ', $this->showTypeDetails('#VALUE!')); + $this->debugLog->writeDebugLog('Evaluation Result is a %s', $this->showTypeDetails('#VALUE!')); return false; } @@ -4952,29 +5046,28 @@ class Calculation } /** - * @param null|string $cellID * @param mixed $operand1 * @param mixed $operand2 * @param string $operation * * @return array */ - private function executeArrayComparison($cellID, $operand1, $operand2, $operation, Stack &$stack, bool $recursingArrays) + private function executeArrayComparison($operand1, $operand2, $operation, Stack &$stack, bool $recursingArrays) { $result = []; if (!is_array($operand2)) { // Operand 1 is an array, Operand 2 is a scalar foreach ($operand1 as $x => $operandData) { - $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2)); - $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack); + $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2)); + $this->executeBinaryComparisonOperation($operandData, $operand2, $operation, $stack); $r = $stack->pop(); $result[$x] = $r['value']; } } elseif (!is_array($operand1)) { // Operand 1 is a scalar, Operand 2 is an array foreach ($operand2 as $x => $operandData) { - $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData)); - $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack); + $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operand1), $operation, $this->showValue($operandData)); + $this->executeBinaryComparisonOperation($operand1, $operandData, $operation, $stack); $r = $stack->pop(); $result[$x] = $r['value']; } @@ -4984,14 +5077,14 @@ class Calculation self::checkMatrixOperands($operand1, $operand2, 2); } foreach ($operand1 as $x => $operandData) { - $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2[$x])); - $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2[$x], $operation, $stack, true); + $this->debugLog->writeDebugLog('Evaluating Comparison %s %s %s', $this->showValue($operandData), $operation, $this->showValue($operand2[$x])); + $this->executeBinaryComparisonOperation($operandData, $operand2[$x], $operation, $stack, true); $r = $stack->pop(); $result[$x] = $r['value']; } } // Log the result details - $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result)); + $this->debugLog->writeDebugLog('Comparison Evaluation Result is %s', $this->showTypeDetails($result)); // And push the result onto the stack $stack->push('Array', $result); @@ -4999,7 +5092,6 @@ class Calculation } /** - * @param null|string $cellID * @param mixed $operand1 * @param mixed $operand2 * @param string $operation @@ -5007,135 +5099,23 @@ class Calculation * * @return mixed */ - private function executeBinaryComparisonOperation($cellID, $operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false) + private function executeBinaryComparisonOperation($operand1, $operand2, $operation, Stack &$stack, $recursingArrays = false) { // If we're dealing with matrix operations, we want a matrix result if ((is_array($operand1)) || (is_array($operand2))) { - return $this->executeArrayComparison($cellID, $operand1, $operand2, $operation, $stack, $recursingArrays); + return $this->executeArrayComparison($operand1, $operand2, $operation, $stack, $recursingArrays); } - // Simple validate the two operands if they are string values - if (is_string($operand1) && $operand1 > '' && $operand1[0] == self::FORMULA_STRING_QUOTE) { - $operand1 = self::unwrapResult($operand1); - } - if (is_string($operand2) && $operand2 > '' && $operand2[0] == self::FORMULA_STRING_QUOTE) { - $operand2 = self::unwrapResult($operand2); - } - - // Use case insensitive comparaison if not OpenOffice mode - if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) { - if (is_string($operand1)) { - $operand1 = Shared\StringHelper::strToUpper($operand1); - } - if (is_string($operand2)) { - $operand2 = Shared\StringHelper::strToUpper($operand2); - } - } - - $useLowercaseFirstComparison = is_string($operand1) && is_string($operand2) && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE; - - // execute the necessary operation - switch ($operation) { - // Greater than - case '>': - if ($useLowercaseFirstComparison) { - $result = $this->strcmpLowercaseFirst($operand1, $operand2) > 0; - } else { - $result = ($operand1 > $operand2); - } - - break; - // Less than - case '<': - if ($useLowercaseFirstComparison) { - $result = $this->strcmpLowercaseFirst($operand1, $operand2) < 0; - } else { - $result = ($operand1 < $operand2); - } - - break; - // Equality - case '=': - if (is_numeric($operand1) && is_numeric($operand2)) { - $result = (abs($operand1 - $operand2) < $this->delta); - } else { - $result = $this->strcmpAllowNull($operand1, $operand2) == 0; - } - - break; - // Greater than or equal - case '>=': - if (is_numeric($operand1) && is_numeric($operand2)) { - $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 > $operand2)); - } elseif ($useLowercaseFirstComparison) { - $result = $this->strcmpLowercaseFirst($operand1, $operand2) >= 0; - } else { - $result = $this->strcmpAllowNull($operand1, $operand2) >= 0; - } - - break; - // Less than or equal - case '<=': - if (is_numeric($operand1) && is_numeric($operand2)) { - $result = ((abs($operand1 - $operand2) < $this->delta) || ($operand1 < $operand2)); - } elseif ($useLowercaseFirstComparison) { - $result = $this->strcmpLowercaseFirst($operand1, $operand2) <= 0; - } else { - $result = $this->strcmpAllowNull($operand1, $operand2) <= 0; - } - - break; - // Inequality - case '<>': - if (is_numeric($operand1) && is_numeric($operand2)) { - $result = (abs($operand1 - $operand2) > 1E-14); - } else { - $result = $this->strcmpAllowNull($operand1, $operand2) != 0; - } - - break; - - default: - throw new Exception('Unsupported binary comparison operation'); - } + $result = BinaryComparison::compare($operand1, $operand2, $operation); // Log the result details - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); // And push the result onto the stack $stack->push('Value', $result); return $result; } - /** - * Compare two strings in the same way as strcmp() except that lowercase come before uppercase letters. - * - * @param null|string $str1 First string value for the comparison - * @param null|string $str2 Second string value for the comparison - * - * @return int - */ - private function strcmpLowercaseFirst($str1, $str2) - { - $inversedStr1 = Shared\StringHelper::strCaseReverse($str1); - $inversedStr2 = Shared\StringHelper::strCaseReverse($str2); - - return strcmp($inversedStr1 ?? '', $inversedStr2 ?? ''); - } - - /** - * PHP8.1 deprecates passing null to strcmp. - * - * @param null|string $str1 First string value for the comparison - * @param null|string $str2 Second string value for the comparison - * - * @return int - */ - private function strcmpAllowNull($str1, $str2) - { - return strcmp($str1 ?? '', $str2 ?? ''); - } - /** * @param mixed $operand1 * @param mixed $operand2 @@ -5169,7 +5149,7 @@ class Calculation $matrixResult = $matrix->$matrixFunction($operand2); $result = $matrixResult->getArray(); } catch (\Exception $ex) { - $this->debugLog->writeDebugLog('JAMA Matrix Exception: ', $ex->getMessage()); + $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); $result = '#VALUE!'; } } else { @@ -5178,7 +5158,7 @@ class Calculation ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) || (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0)) ) { - $result = Functions::VALUE(); + $result = Information\ExcelError::VALUE(); } else { // If we're dealing with non-matrix operations, execute the necessary operation switch ($operation) { @@ -5201,8 +5181,8 @@ class Calculation case '/': if ($operand2 == 0) { // Trap for Divide by Zero error - $stack->push('Error', '#DIV/0!'); - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails('#DIV/0!')); + $stack->push('Error', ExcelError::DIV0()); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails(ExcelError::DIV0())); return false; } @@ -5222,7 +5202,7 @@ class Calculation } // Log the result details - $this->debugLog->writeDebugLog('Evaluation Result is ', $this->showTypeDetails($result)); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); // And push the result onto the stack $stack->push('Value', $result); @@ -5275,8 +5255,6 @@ class Calculation $aReferences = Coordinate::extractAllCellReferencesInRange($range); $range = "'" . $worksheetName . "'" . '!' . $range; if (!isset($aReferences[1])) { - $currentCol = ''; - $currentRow = 0; // Single cell in range sscanf($aReferences[0], '%[A-Z]%d', $currentCol, $currentRow); if ($worksheet->cellExists($aReferences[0])) { @@ -5287,8 +5265,6 @@ class Calculation } else { // Extract cell data for all cells in the range foreach ($aReferences as $reference) { - $currentCol = ''; - $currentRow = 0; // Extract range sscanf($reference, '%[A-Z]%d', $currentCol, $currentRow); if ($worksheet->cellExists($reference)) { @@ -5324,9 +5300,9 @@ class Calculation } // Named range? - $namedRange = DefinedName::resolveName($range, $worksheet); + $namedRange = DefinedName::resolveName($range, /** @scrutinizer ignore-type */ $worksheet); if ($namedRange === null) { - return Functions::REF(); + return Information\ExcelError::REF(); } $worksheet = $namedRange->getWorksheet(); @@ -5485,14 +5461,6 @@ class Calculation return $args; } - private function getUnusedBranchStoreKey() - { - $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter; - ++$this->branchStoreKeyCounter; - - return $storeKeyValue; - } - private function getTokensAsString($tokens) { $tokensStr = array_map(function ($token) { @@ -5515,7 +5483,7 @@ class Calculation $definedNameScope = $namedRange->getScope(); if ($definedNameScope !== null && $definedNameScope !== $cellWorksheet) { // The defined name isn't in our current scope, so #REF - $result = Functions::REF(); + $result = Information\ExcelError::REF(); $stack->push('Error', $result, $namedRange->getName()); return $result; @@ -5529,7 +5497,7 @@ class Calculation $definedNameValue = '=' . $definedNameValue; } - $this->debugLog->writeDebugLog("Defined Name is a {$definedNameType} with a value of {$definedNameValue}"); + $this->debugLog->writeDebugLog('Defined Name is a %s with a value of %s', $definedNameType, $definedNameValue); $recursiveCalculationCell = ($definedNameWorksheet !== null && $definedNameWorksheet !== $cellWorksheet) ? $definedNameWorksheet->getCell('A1') @@ -5543,7 +5511,7 @@ class Calculation $cell->getRow() - 1 ); - $this->debugLog->writeDebugLog("Value adjusted for relative references is {$definedNameValue}"); + $this->debugLog->writeDebugLog('Value adjusted for relative references is %s', $definedNameValue); $recursiveCalculator = new self($this->spreadsheet); $recursiveCalculator->getDebugLog()->setWriteDebugLog($this->getDebugLog()->getWriteDebugLog()); @@ -5552,7 +5520,7 @@ class Calculation if ($this->getDebugLog()->getWriteDebugLog()) { $this->debugLog->mergeDebugLog(array_slice($recursiveCalculator->getDebugLog()->getLog(), 3)); - $this->debugLog->writeDebugLog("Evaluation Result for Named {$definedNameType} {$namedRange->getName()} is {$this->showTypeDetails($result)}"); + $this->debugLog->writeDebugLog('Evaluation Result for Named %s %s is %s', $definedNameType, $namedRange->getName(), $this->showTypeDetails($result)); } $stack->push('Defined Name', $result, $namedRange->getName()); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database/DGet.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database/DGet.php index c2ffe30265a..abe06e65c37 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database/DGet.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Database/DGet.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class DGet extends DatabaseAbstract { @@ -41,7 +41,7 @@ class DGet extends DatabaseAbstract $columnData = self::getFilteredColumn($database, $field, $criteria); if (count($columnData) > 1) { - return Functions::NAN(); + return ExcelError::NAN(); } $row = array_pop($columnData); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTime.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTime.php index 44a38c19e01..79fa8dc0bbf 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTime.php @@ -278,9 +278,9 @@ class DateTime * or a standard date string * @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object * or a standard date string - * @param string $unit + * @param array|string $unit * - * @return int|string Interval between the dates + * @return array|int|string Interval between the dates */ public static function DATEDIF($startDate = 0, $endDate = 0, $unit = 'D') { @@ -300,12 +300,12 @@ class DateTime * @See DateTimeExcel\Days::between() * Use the between method in the DateTimeExcel\Days class instead * - * @param DateTimeInterface|float|int|string $endDate Excel date serial value (float), + * @param array|DateTimeInterface|float|int|string $endDate Excel date serial value (float), * PHP date timestamp (integer), PHP DateTime object, or a standard date string - * @param DateTimeInterface|float|int|string $startDate Excel date serial value (float), + * @param array|DateTimeInterface|float|int|string $startDate Excel date serial value (float), * PHP date timestamp (integer), PHP DateTime object, or a standard date string * - * @return int|string Number of days between start date and end date or an error + * @return array|int|string Number of days between start date and end date or an error */ public static function DAYS($endDate = 0, $startDate = 0) { @@ -331,7 +331,7 @@ class DateTime * PHP DateTime object, or a standard date string * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param bool $method US or European Method + * @param array|bool $method US or European Method * FALSE or omitted: U.S. (NASD) method. If the starting date is * the last day of a month, it becomes equal to the 30th of the * same month. If the ending date is the last day of a month and @@ -343,7 +343,7 @@ class DateTime * occur on the 31st of a month become equal to the 30th of the * same month. * - * @return int|string Number of days between start date and end date + * @return array|int|string Number of days between start date and end date */ public static function DAYS360($startDate = 0, $endDate = 0, $method = false) { @@ -373,14 +373,14 @@ class DateTime * PHP DateTime object, or a standard date string * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param int $method Method used for the calculation + * @param array|int $method Method used for the calculation * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 * 3 Actual/365 * 4 European 30/360 * - * @return float|string fraction of the year, or a string containing an error + * @return array|float|string fraction of the year, or a string containing an error */ public static function YEARFRAC($startDate = 0, $endDate = 0, $method = 0) { @@ -409,7 +409,7 @@ class DateTime * PHP DateTime object, or a standard date string * @param mixed $dateArgs * - * @return int|string Interval between the dates + * @return array|int|string Interval between the dates */ public static function NETWORKDAYS($startDate, $endDate, ...$dateArgs) { @@ -464,7 +464,7 @@ class DateTime * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string * - * @return int|string Day of the month + * @return array|int|string Day of the month */ public static function DAYOFMONTH($dateValue = 1) { @@ -492,7 +492,7 @@ class DateTime * 2 Numbers 1 (Monday) through 7 (Sunday). * 3 Numbers 0 (Monday) through 6 (Sunday). * - * @return int|string Day of the week value + * @return array|int|string Day of the week value */ public static function WEEKDAY($dateValue = 1, $style = 1) { @@ -704,7 +704,7 @@ class DateTime * 17 Week begins on Sunday. * 21 ISO (Jan. 4 is week 1, begins on Monday). * - * @return int|string Week Number + * @return array|int|string Week Number */ public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) { @@ -727,7 +727,7 @@ class DateTime * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string * - * @return int|string Week Number + * @return array|int|string Week Number */ public static function ISOWEEKNUM($dateValue = 1) { @@ -751,7 +751,7 @@ class DateTime * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string * - * @return int|string Month of the year + * @return array|int|string Month of the year */ public static function MONTHOFYEAR($dateValue = 1) { @@ -775,7 +775,7 @@ class DateTime * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string * - * @return int|string Year + * @return array|int|string Year */ public static function YEAR($dateValue = 1) { @@ -799,7 +799,7 @@ class DateTime * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard time string * - * @return int|string Hour + * @return array|int|string Hour */ public static function HOUROFDAY($timeValue = 0) { @@ -823,7 +823,7 @@ class DateTime * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard time string * - * @return int|string Minute + * @return array|int|string Minute */ public static function MINUTE($timeValue = 0) { @@ -847,7 +847,7 @@ class DateTime * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard time string * - * @return int|string Second + * @return array|int|string Second */ public static function SECOND($timeValue = 0) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Current.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Current.php index d23ce37b739..5de671d3cb1 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Current.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Current.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use DateTimeImmutable; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Current { @@ -29,7 +29,7 @@ class Current $dti = new DateTimeImmutable(); $dateArray = Helpers::dateParse($dti->format('c')); - return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray, true) : Functions::VALUE(); + return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray, true) : ExcelError::VALUE(); } /** @@ -54,6 +54,6 @@ class Current $dti = new DateTimeImmutable(); $dateArray = Helpers::dateParse($dti->format('c')); - return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray) : Functions::VALUE(); + return Helpers::dateParseSucceeded($dateArray) ? Helpers::returnIn3FormatsArray($dateArray) : ExcelError::VALUE(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php index d18e2371c3f..56982c5c71d 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Date.php @@ -2,13 +2,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Date { + use ArrayEnabled; + /** * DATE. * @@ -24,7 +27,7 @@ class Date * A Month name or abbreviation (English only at this point) such as 'January' or 'Jan' will still be accepted, * as will a day value with a suffix (e.g. '21st' rather than simply 21); again only English language. * - * @param int $year The value of the year argument can include one to four digits. + * @param array|int $year The value of the year argument can include one to four digits. * Excel interprets the year argument according to the configured * date system: 1900 or 1904. * If year is between 0 (zero) and 1899 (inclusive), Excel adds that @@ -35,7 +38,7 @@ class Date * 2008. * If year is less than 0 or is 10000 or greater, Excel returns the * #NUM! error value. - * @param int $month A positive or negative integer representing the month of the year + * @param array|int $month A positive or negative integer representing the month of the year * from 1 to 12 (January to December). * If month is greater than 12, month adds that number of months to * the first month in the year specified. For example, DATE(2008,14,2) @@ -44,7 +47,7 @@ class Date * number of months, plus 1, from the first month in the year * specified. For example, DATE(2008,-3,2) returns the serial number * representing September 2, 2007. - * @param int $day A positive or negative integer representing the day of the month + * @param array|int $day A positive or negative integer representing the day of the month * from 1 to 31. * If day is greater than the number of days in the month specified, * day adds that number of days to the first day in the month. For @@ -57,9 +60,15 @@ class Date * * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function fromYMD($year, $month, $day) { + if (is_array($year) || is_array($month) || is_array($day)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $year, $month, $day); + } + $baseYear = SharedDateHelper::getExcelCalendar(); try { @@ -84,18 +93,17 @@ class Date */ private static function getYear($year, int $baseYear): int { - $year = Functions::flattenSingleValue($year); $year = ($year !== null) ? StringHelper::testStringAsNumeric((string) $year) : 0; if (!is_numeric($year)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } $year = (int) $year; if ($year < ($baseYear - 1900)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } if ((($baseYear - 1900) !== 0) && ($year < $baseYear) && ($year >= 1900)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } if (($year < $baseYear) && ($year >= ($baseYear - 1900))) { @@ -112,15 +120,13 @@ class Date */ private static function getMonth($month): int { - $month = Functions::flattenSingleValue($month); - if (($month !== null) && (!is_numeric($month))) { $month = SharedDateHelper::monthStringToNumber($month); } $month = ($month !== null) ? StringHelper::testStringAsNumeric((string) $month) : 0; if (!is_numeric($month)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (int) $month; @@ -133,15 +139,13 @@ class Date */ private static function getDay($day): int { - $day = Functions::flattenSingleValue($day); - if (($day !== null) && (!is_numeric($day))) { $day = SharedDateHelper::dayStringToNumber($day); } $day = ($day !== null) ? StringHelper::testStringAsNumeric((string) $day) : 0; if (!is_numeric($day)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (int) $day; @@ -162,7 +166,7 @@ class Date // Re-validate the year parameter after adjustments if (($year < $baseYear) || ($year >= 10000)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php index 37ea0315154..b669eb0a1e4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateParts.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class DateParts { + use ArrayEnabled; + /** * DAYOFMONTH. * @@ -19,11 +22,18 @@ class DateParts * * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string + * Or can be an array of date values * - * @return int|string Day of the month + * @return array|int|string Day of the month + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function day($dateValue) { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + $weirdResult = self::weirdCondition($dateValue); if ($weirdResult >= 0) { return $weirdResult; @@ -52,11 +62,18 @@ class DateParts * * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string + * Or can be an array of date values * - * @return int|string Month of the year + * @return array|int|string Month of the year + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function month($dateValue) { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + try { $dateValue = Helpers::getDateValue($dateValue); } catch (Exception $e) { @@ -83,11 +100,18 @@ class DateParts * * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string + * Or can be an array of date values * - * @return int|string Year + * @return array|int|string Year + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function year($dateValue) { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + try { $dateValue = Helpers::getDateValue($dateValue); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index 3b21550626a..52543a72dd3 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -3,11 +3,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use DateTimeImmutable; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class DateValue { + use ArrayEnabled; + /** * DATEVALUE. * @@ -21,7 +24,7 @@ class DateValue * Excel Function: * DATEVALUE(dateValue) * - * @param string $dateValue Text that represents a date in a Microsoft Excel date format. + * @param array|string $dateValue Text that represents a date in a Microsoft Excel date format. * For example, "1/30/2008" or "30-Jan-2008" are text strings within * quotation marks that represent dates. Using the default date * system in Excel for Windows, date_text must represent a date from @@ -29,17 +32,24 @@ class DateValue * system in Excel for the Macintosh, date_text must represent a date * from January 1, 1904, to December 31, 9999. DATEVALUE returns the * #VALUE! error value if date_text is out of this range. + * Or can be an array of date values * * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function fromString($dateValue) { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + $dti = new DateTimeImmutable(); $baseYear = SharedDateHelper::getExcelCalendar(); - $dateValue = trim(Functions::flattenSingleValue($dateValue ?? ''), '"'); + $dateValue = trim($dateValue ?? '', '"'); // Strip any ordinals because they're allowed in Excel (English only) - $dateValue = preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue) ?? ''; + $dateValue = (string) preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); // Convert separators (/ . or space) to hyphens (should also handle dot used for ordinals in some countries, e.g. Denmark, Germany) $dateValue = str_replace(['/', '.', '-', ' '], ' ', $dateValue); @@ -49,7 +59,7 @@ class DateValue foreach ($t1 as &$t) { if ((is_numeric($t)) && ($t > 31)) { if ($yearFound) { - return Functions::VALUE(); + return ExcelError::VALUE(); } if ($t < 100) { $t += 1900; @@ -59,7 +69,7 @@ class DateValue } if (count($t1) === 1) { // We've been fed a time value without any date - return ((strpos((string) $t, ':') === false)) ? Functions::Value() : 0.0; + return ((strpos((string) $t, ':') === false)) ? ExcelError::Value() : 0.0; } unset($t); @@ -121,12 +131,12 @@ class DateValue */ private static function finalResults(array $PHPDateArray, DateTimeImmutable $dti, int $baseYear) { - $retValue = Functions::Value(); + $retValue = ExcelError::Value(); if (Helpers::dateParseSucceeded($PHPDateArray)) { // Execute function Helpers::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y')); if ($PHPDateArray['year'] < $baseYear) { - return Functions::VALUE(); + return ExcelError::VALUE(); } Helpers::replaceIfEmpty($PHPDateArray['month'], $dti->format('m')); Helpers::replaceIfEmpty($PHPDateArray['day'], $dti->format('d')); @@ -137,7 +147,7 @@ class DateValue $day = (int) $PHPDateArray['day']; $year = (int) $PHPDateArray['year']; if (!checkdate($month, $day, $year)) { - return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : Functions::VALUE(); + return ($year === 1900 && $month === 2 && $day === 29) ? Helpers::returnIn3FormatsFloat(60.0) : ExcelError::VALUE(); } $retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php index 5a97d8df8ab..a3b9745106f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php @@ -3,12 +3,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use DateTimeInterface; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class Days { + use ArrayEnabled; + /** * DAYS. * @@ -17,15 +20,23 @@ class Days * Excel Function: * DAYS(endDate, startDate) * - * @param DateTimeInterface|float|int|string $endDate Excel date serial value (float), - * PHP date timestamp (integer), PHP DateTime object, or a standard date string - * @param DateTimeInterface|float|int|string $startDate Excel date serial value (float), - * PHP date timestamp (integer), PHP DateTime object, or a standard date string + * @param array|DateTimeInterface|float|int|string $endDate Excel date serial value (float), + * PHP date timestamp (integer), PHP DateTime object, or a standard date string + * Or can be an array of date values + * @param array|DateTimeInterface|float|int|string $startDate Excel date serial value (float), + * PHP date timestamp (integer), PHP DateTime object, or a standard date string + * Or can be an array of date values * - * @return int|string Number of days between start date and end date or an error + * @return array|int|string Number of days between start date and end date or an error + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions */ public static function between($endDate, $startDate) { + if (is_array($endDate) || is_array($startDate)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $endDate, $startDate); + } + try { $startDate = Helpers::getDateValue($startDate); $endDate = Helpers::getDateValue($endDate); @@ -37,7 +48,7 @@ class Days $PHPStartDateObject = SharedDateHelper::excelToDateTimeObject($startDate); $PHPEndDateObject = SharedDateHelper::excelToDateTimeObject($endDate); - $days = Functions::VALUE(); + $days = ExcelError::VALUE(); $diff = $PHPStartDateObject->diff($PHPEndDateObject); if ($diff !== false && !is_bool($diff->days)) { $days = $diff->days; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php index bbc5d16aaa3..6f71621eaa5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class Days360 { + use ArrayEnabled; + /** * DAYS360. * @@ -18,11 +21,13 @@ class Days360 * Excel Function: * DAYS360(startDate,endDate[,method]) * - * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), + * @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), + * Or can be an array of date values + * @param array|mixed $endDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param mixed $method US or European Method as a bool + * Or can be an array of date values + * @param array|mixed $method US or European Method as a bool * FALSE or omitted: U.S. (NASD) method. If the starting date is * the last day of a month, it becomes equal to the 30th of the * same month. If the ending date is the last day of a month and @@ -33,11 +38,18 @@ class Days360 * TRUE: European method. Starting dates and ending dates that * occur on the 31st of a month become equal to the 30th of the * same month. + * Or can be an array of methods * - * @return int|string Number of days between start date and end date + * @return array|int|string Number of days between start date and end date + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions */ public static function between($startDate = 0, $endDate = 0, $method = false) { + if (is_array($startDate) || is_array($endDate) || is_array($method)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method); + } + try { $startDate = Helpers::getDateValue($startDate); $endDate = Helpers::getDateValue($endDate); @@ -46,7 +58,7 @@ class Days360 } if (!is_bool($method)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } // Execute function diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Difference.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Difference.php index 6adeca179a0..ecb1cf0a61e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Difference.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Difference.php @@ -4,30 +4,42 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use DateInterval; use DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class Difference { + use ArrayEnabled; + /** * DATEDIF. * * @param mixed $startDate Excel date serial value, PHP date/time stamp, PHP DateTime object * or a standard date string + * Or can be an array of date values * @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object * or a standard date string - * @param string $unit + * Or can be an array of date values + * @param array|string $unit + * Or can be an array of unit values * - * @return int|string Interval between the dates + * @return array|int|string Interval between the dates + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions */ public static function interval($startDate, $endDate, $unit = 'D') { + if (is_array($startDate) || is_array($endDate) || is_array($unit)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $unit); + } + try { $startDate = Helpers::getDateValue($startDate); $endDate = Helpers::getDateValue($endDate); $difference = self::initialDiff($startDate, $endDate); - $unit = strtoupper(Functions::flattenSingleValue($unit)); + $unit = strtoupper($unit); } catch (Exception $e) { return $e->getMessage(); } @@ -53,14 +65,14 @@ class Difference $retVal = self::replaceRetValue($retVal, $unit, 'YD') ?? self::datedifYD($difference, $startYears, $endYears, $PHPStartDateObject, $PHPEndDateObject); $retVal = self::replaceRetValue($retVal, $unit, 'YM') ?? self::datedifYM($PHPDiffDateObject); - return is_bool($retVal) ? Functions::VALUE() : $retVal; + return is_bool($retVal) ? ExcelError::VALUE() : $retVal; } private static function initialDiff(float $startDate, float $endDate): float { // Validate parameters if ($startDate > $endDate) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $endDate - $startDate; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php index 55ce13f1d2f..238451534aa 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use DateTime; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class Helpers @@ -33,7 +34,7 @@ class Helpers if (is_object($dateValue)) { $retval = SharedDateHelper::PHPToExcel($dateValue); if (is_bool($retval)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return $retval; @@ -46,11 +47,11 @@ class Helpers $dateValue = DateValue::fromString($dateValue); Functions::setReturnDateType($saveReturnDateType); if (!is_numeric($dateValue)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } } if ($dateValue < 0 && Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return (float) $dateValue; @@ -253,7 +254,7 @@ class Helpers return (float) $number; } - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } /** @@ -266,13 +267,13 @@ class Helpers public static function validateNotNegative($number) { if (!is_numeric($number)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } if ($number >= 0) { return (float) $number; } - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } public static function silly1900(DateTime $PHPDateObject, string $mod = '-1 day'): void diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php index 560b7a80714..c72d006b925 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Month { + use ArrayEnabled; + /** * EDATE. * @@ -19,15 +22,23 @@ class Month * * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param int $adjustmentMonths The number of months before or after start_date. + * Or can be an array of date values + * @param array|int $adjustmentMonths The number of months before or after start_date. * A positive value for months yields a future date; * a negative value yields a past date. + * Or can be an array of adjustment values * - * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, * depending on the value of the ReturnDateType flag + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function adjust($dateValue, $adjustmentMonths) { + if (is_array($dateValue) || is_array($adjustmentMonths)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths); + } + try { $dateValue = Helpers::getDateValue($dateValue, false); $adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths); @@ -54,15 +65,23 @@ class Month * * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param int $adjustmentMonths The number of months before or after start_date. + * Or can be an array of date values + * @param array|int $adjustmentMonths The number of months before or after start_date. * A positive value for months yields a future date; * a negative value yields a past date. + * Or can be an array of adjustment values * - * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, * depending on the value of the ReturnDateType flag + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function lastDay($dateValue, $adjustmentMonths) { + if (is_array($dateValue) || is_array($adjustmentMonths)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $adjustmentMonths); + } + try { $dateValue = Helpers::getDateValue($dateValue, false); $adjustmentMonths = Helpers::validateNumericNull($adjustmentMonths); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php index d0f53cf392c..3b8942bafbf 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class NetworkDays { + use ArrayEnabled; + /** * NETWORKDAYS. * @@ -20,14 +23,28 @@ class NetworkDays * * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string + * Or can be an array of date values * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param mixed $dateArgs + * Or can be an array of date values + * @param mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation * - * @return int|string Interval between the dates + * @return array|int|string Interval between the dates + * If an array of values is passed for the $startDate or $endDate arguments, then the returned result + * will also be an array with matching dimensions */ public static function count($startDate, $endDate, ...$dateArgs) { + if (is_array($startDate) || is_array($endDate)) { + return self::evaluateArrayArgumentsSubset( + [self::class, __FUNCTION__], + 2, + $startDate, + $endDate, + ...$dateArgs + ); + } + try { // Retrieve the mandatory start and end date that are referenced in the function definition $sDate = Helpers::getDateValue($startDate); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php index fb5e49652f8..4ff71983f48 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php @@ -3,12 +3,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class Time { + use ArrayEnabled; + /** * TIME. * @@ -20,23 +24,31 @@ class Time * Excel Function: * TIME(hour,minute,second) * - * @param mixed $hour A number from 0 (zero) to 32767 representing the hour. + * @param array|int $hour A number from 0 (zero) to 32767 representing the hour. * Any value greater than 23 will be divided by 24 and the remainder * will be treated as the hour value. For example, TIME(27,0,0) = * TIME(3,0,0) = .125 or 3:00 AM. - * @param mixed $minute A number from 0 to 32767 representing the minute. + * @param array|int $minute A number from 0 to 32767 representing the minute. * Any value greater than 59 will be converted to hours and minutes. * For example, TIME(0,750,0) = TIME(12,30,0) = .520833 or 12:30 PM. - * @param mixed $second A number from 0 to 32767 representing the second. + * @param array|int $second A number from 0 to 32767 representing the second. * Any value greater than 59 will be converted to hours, minutes, * and seconds. For example, TIME(0,0,2000) = TIME(0,33,22) = .023148 * or 12:33:20 AM + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions * - * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function fromHMS($hour, $minute, $second) { + if (is_array($hour) || is_array($minute) || is_array($second)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $hour, $minute, $second); + } + try { $hour = self::toIntWithNullBool($hour); $minute = self::toIntWithNullBool($minute); @@ -51,7 +63,7 @@ class Time if ($hour > 23) { $hour = $hour % 24; } elseif ($hour < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } // Execute function @@ -105,13 +117,12 @@ class Time */ private static function toIntWithNullBool($value): int { - $value = Functions::flattenSingleValue($value); $value = $value ?? 0; if (is_bool($value)) { $value = (int) $value; } if (!is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (int) $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php index 49cd983d874..d9b99f3c364 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeParts.php @@ -2,12 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class TimeParts { + use ArrayEnabled; + /** * HOUROFDAY. * @@ -19,13 +21,19 @@ class TimeParts * * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard time string + * Or can be an array of date/time values * - * @return int|string Hour + * @return array|int|string Hour + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function hour($timeValue) { + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + try { - $timeValue = Functions::flattenSingleValue($timeValue); Helpers::nullFalseTrueToNumber($timeValue); if (!is_numeric($timeValue)) { $timeValue = Helpers::getTimeValue($timeValue); @@ -53,13 +61,19 @@ class TimeParts * * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard time string + * Or can be an array of date/time values * - * @return int|string Minute + * @return array|int|string Minute + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function minute($timeValue) { + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + try { - $timeValue = Functions::flattenSingleValue($timeValue); Helpers::nullFalseTrueToNumber($timeValue); if (!is_numeric($timeValue)) { $timeValue = Helpers::getTimeValue($timeValue); @@ -87,13 +101,19 @@ class TimeParts * * @param mixed $timeValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard time string + * Or can be an array of date/time values * - * @return int|string Second + * @return array|int|string Second + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function second($timeValue) { + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + try { - $timeValue = Functions::flattenSingleValue($timeValue); Helpers::nullFalseTrueToNumber($timeValue); if (!is_numeric($timeValue)) { $timeValue = Helpers::getTimeValue($timeValue); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php index c9645e66573..aa27206b96f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php @@ -3,11 +3,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use Datetime; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class TimeValue { + use ArrayEnabled; + /** * TIMEVALUE. * @@ -21,17 +25,24 @@ class TimeValue * Excel Function: * TIMEVALUE(timeValue) * - * @param string $timeValue A text string that represents a time in any one of the Microsoft + * @param array|string $timeValue A text string that represents a time in any one of the Microsoft * Excel time formats; for example, "6:45 PM" and "18:45" text strings * within quotation marks that represent time. * Date information in time_text is ignored. + * Or can be an array of date/time values * * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, * depending on the value of the ReturnDateType flag + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function fromString($timeValue) { - $timeValue = trim(Functions::flattenSingleValue($timeValue ?? ''), '"'); + if (is_array($timeValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $timeValue); + } + + $timeValue = trim($timeValue ?? '', '"'); $timeValue = str_replace(['/', '.'], '-', $timeValue); $arraySplit = preg_split('/[\/:\-\s]/', $timeValue) ?: []; @@ -41,7 +52,7 @@ class TimeValue } $PHPDateArray = Helpers::dateParse($timeValue); - $retValue = Functions::VALUE(); + $retValue = ExcelError::VALUE(); if (Helpers::dateParseSucceeded($PHPDateArray)) { /** @var int */ $hour = $PHPDateArray['hour']; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Week.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Week.php index 66362221049..2f69007d131 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Week.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/Week.php @@ -3,12 +3,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class Week { + use ArrayEnabled; + /** * WEEKNUM. * @@ -24,7 +27,8 @@ class Week * * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param int $method Week begins on Sunday or Monday + * Or can be an array of date values + * @param array|int $method Week begins on Sunday or Monday * 1 or omitted Week begins on Sunday. * 2 Week begins on Monday. * 11 Week begins on Monday. @@ -35,11 +39,18 @@ class Week * 16 Week begins on Saturday. * 17 Week begins on Sunday. * 21 ISO (Jan. 4 is week 1, begins on Monday). + * Or can be an array of methods * - * @return int|string Week Number + * @return array|int|string Week Number + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function number($dateValue, $method = Constants::STARTWEEK_SUNDAY) { + if (is_array($dateValue) || is_array($method)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $method); + } + $origDateValueNull = empty($dateValue); try { @@ -88,11 +99,18 @@ class Week * * @param mixed $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string + * Or can be an array of date values * - * @return int|string Week Number + * @return array|int|string Week Number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function isoWeekNumber($dateValue) { + if (is_array($dateValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $dateValue); + } + if (self::apparentBug($dateValue)) { return 52; } @@ -119,17 +137,25 @@ class Week * Excel Function: * WEEKDAY(dateValue[,style]) * - * @param null|float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), + * @param null|array|float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string + * Or can be an array of date values * @param mixed $style A number that determines the type of return value * 1 or omitted Numbers 1 (Sunday) through 7 (Saturday). * 2 Numbers 1 (Monday) through 7 (Sunday). * 3 Numbers 0 (Monday) through 6 (Sunday). + * Or can be an array of styles * - * @return int|string Day of the week value + * @return array|int|string Day of the week value + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function day($dateValue, $style = 1) { + if (is_array($dateValue) || is_array($style)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dateValue, $style); + } + try { $dateValue = Helpers::getDateValue($dateValue); $style = self::validateStyle($style); @@ -165,14 +191,12 @@ class Week */ private static function validateStyle($style): int { - $style = Functions::flattenSingleValue($style); - if (!is_numeric($style)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } $style = (int) $style; if (($style < 1) || ($style > 3)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $style; @@ -209,7 +233,7 @@ class Week private static function validateDateValue($dateValue): float { if (is_bool($dateValue)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return Helpers::getDateValue($dateValue); @@ -225,14 +249,14 @@ class Week if ($method === null) { $method = Constants::STARTWEEK_SUNDAY; } - $method = Functions::flattenSingleValue($method); + if (!is_numeric($method)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } $method = (int) $method; if (!array_key_exists($method, Constants::METHODARR)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } $method = Constants::METHODARR[$method]; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php index 89e47b9690b..1f5735e8ebe 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class WorkDay { + use ArrayEnabled; + /** * WORKDAY. * @@ -18,27 +21,37 @@ class WorkDay * Excel Function: * WORKDAY(startDate,endDays[,holidays[,holiday[,...]]]) * - * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), + * @param array|mixed $startDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param int $endDays The number of nonweekend and nonholiday days before or after + * Or can be an array of date values + * @param array|int $endDays The number of nonweekend and nonholiday days before or after * startDate. A positive value for days yields a future date; a * negative value yields a past date. - * @param mixed $dateArgs + * Or can be an array of int values + * @param null|mixed $dateArgs An array of dates (such as holidays) to exclude from the calculation * - * @return mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, + * @return array|mixed Excel date/time serial value, PHP date/time serial value or PHP date/time object, * depending on the value of the ReturnDateType flag + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions */ public static function date($startDate, $endDays, ...$dateArgs) { + if (is_array($startDate) || is_array($endDays)) { + return self::evaluateArrayArgumentsSubset( + [self::class, __FUNCTION__], + 2, + $startDate, + $endDays, + ...$dateArgs + ); + } + // Retrieve the mandatory start date and days that are referenced in the function definition try { $startDate = Helpers::getDateValue($startDate); $endDays = Helpers::validateNumericNull($endDays); - $dateArgs = Functions::flattenArray($dateArgs); - $holidayArray = []; - foreach ($dateArgs as $holidayDate) { - $holidayArray[] = Helpers::getDateValue($holidayDate); - } + $holidayArray = array_map([Helpers::class, 'getDateValue'], Functions::flattenArray($dateArgs)); } catch (Exception $e) { return $e->getMessage(); } @@ -64,9 +77,8 @@ class WorkDay private static function incrementing(float $startDate, int $endDays, array $holidayArray) { // Adjust the start date if it falls over a weekend - $startDoW = self::getWeekDay($startDate, 3); - if (self::getWeekDay($startDate, 3) >= 5) { + if ($startDoW >= 5) { $startDate += 7 - $startDoW; --$endDays; } @@ -126,9 +138,8 @@ class WorkDay private static function decrementing(float $startDate, int $endDays, array $holidayArray) { // Adjust the start date if it falls over a weekend - $startDoW = self::getWeekDay($startDate, 3); - if (self::getWeekDay($startDate, 3) >= 5) { + if ($startDoW >= 5) { $startDate += -$startDoW + 4; ++$endDays; } @@ -172,6 +183,7 @@ class WorkDay } // Adjust the calculated end date if it falls over a weekend $endDoW = self::getWeekDay($endDate, 3); + /** int $endDoW */ if ($endDoW >= 5) { $endDate += -$endDoW + 4; } @@ -182,8 +194,8 @@ class WorkDay private static function getWeekDay(float $date, int $wd): int { - $result = Week::day($date, $wd); + $result = Functions::scalar(Week::day($date, $wd)); - return is_string($result) ? -1 : $result; + return is_int($result) ? $result : -1; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php index da2ac12fc5a..394c6b7320b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php @@ -2,12 +2,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDateHelper; class YearFrac { + use ArrayEnabled; + /** * YEARFRAC. * @@ -23,19 +27,28 @@ class YearFrac * * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string + * Or can be an array of values * @param mixed $endDate Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string - * @param int $method Method used for the calculation + * Or can be an array of methods + * @param array|int $method Method used for the calculation * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 * 3 Actual/365 * 4 European 30/360 + * Or can be an array of methods * - * @return float|string fraction of the year, or a string containing an error + * @return array|float|string fraction of the year, or a string containing an error + * If an array of values is passed for the $startDate or $endDays,arguments, then the returned result + * will also be an array with matching dimensions */ public static function fraction($startDate, $endDate, $method = 0) { + if (is_array($startDate) || is_array($endDate) || is_array($method)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $startDate, $endDate, $method); + } + try { $method = (int) Helpers::validateNumericNull($method); $sDate = Helpers::getDateValue($startDate); @@ -50,18 +63,18 @@ class YearFrac switch ($method) { case 0: - return Days360::between($startDate, $endDate) / 360; + return Functions::scalar(Days360::between($startDate, $endDate)) / 360; case 1: return self::method1($startDate, $endDate); case 2: - return Difference::interval($startDate, $endDate) / 360; + return Functions::scalar(Difference::interval($startDate, $endDate)) / 360; case 3: - return Difference::interval($startDate, $endDate) / 365; + return Functions::scalar(Difference::interval($startDate, $endDate)) / 365; case 4: - return Days360::between($startDate, $endDate, true) / 360; + return Functions::scalar(Days360::between($startDate, $endDate, true)) / 360; } - return Functions::NAN(); + return ExcelError::NAN(); } /** @@ -87,7 +100,7 @@ class YearFrac private static function method1(float $startDate, float $endDate): float { - $days = Difference::interval($startDate, $endDate); + $days = Functions::scalar(Difference::interval($startDate, $endDate)); $startYear = (int) DateParts::year($startDate); $endYear = (int) DateParts::year($endDate); $years = $endYear - $startYear + 1; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php new file mode 100644 index 00000000000..fae9d90097f --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php @@ -0,0 +1,209 @@ +indexStart = (int) array_shift($keys); + $this->rows = $this->rows($arguments); + $this->columns = $this->columns($arguments); + + $this->argumentCount = count($arguments); + $this->arguments = $this->flattenSingleCellArrays($arguments, $this->rows, $this->columns); + + $this->rows = $this->rows($arguments); + $this->columns = $this->columns($arguments); + + if ($this->arrayArguments() > 2) { + throw new Exception('Formulae with more than two array arguments are not supported'); + } + } + + public function arguments(): array + { + return $this->arguments; + } + + public function hasArrayArgument(): bool + { + return $this->arrayArguments() > 0; + } + + public function getFirstArrayArgumentNumber(): int + { + $rowArrays = $this->filterArray($this->rows); + $columnArrays = $this->filterArray($this->columns); + + for ($index = $this->indexStart; $index < $this->argumentCount; ++$index) { + if (isset($rowArrays[$index]) || isset($columnArrays[$index])) { + return ++$index; + } + } + + return 0; + } + + public function getSingleRowVector(): ?int + { + $rowVectors = $this->getRowVectors(); + + return count($rowVectors) === 1 ? array_pop($rowVectors) : null; + } + + private function getRowVectors(): array + { + $rowVectors = []; + for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) { + if ($this->rows[$index] === 1 && $this->columns[$index] > 1) { + $rowVectors[] = $index; + } + } + + return $rowVectors; + } + + public function getSingleColumnVector(): ?int + { + $columnVectors = $this->getColumnVectors(); + + return count($columnVectors) === 1 ? array_pop($columnVectors) : null; + } + + private function getColumnVectors(): array + { + $columnVectors = []; + for ($index = $this->indexStart; $index < ($this->indexStart + $this->argumentCount); ++$index) { + if ($this->rows[$index] > 1 && $this->columns[$index] === 1) { + $columnVectors[] = $index; + } + } + + return $columnVectors; + } + + public function getMatrixPair(): array + { + for ($i = $this->indexStart; $i < ($this->indexStart + $this->argumentCount - 1); ++$i) { + for ($j = $i + 1; $j < $this->argumentCount; ++$j) { + if (isset($this->rows[$i], $this->rows[$j])) { + return [$i, $j]; + } + } + } + + return []; + } + + public function isVector(int $argument): bool + { + return $this->rows[$argument] === 1 || $this->columns[$argument] === 1; + } + + public function isRowVector(int $argument): bool + { + return $this->rows[$argument] === 1; + } + + public function isColumnVector(int $argument): bool + { + return $this->columns[$argument] === 1; + } + + public function rowCount(int $argument): int + { + return $this->rows[$argument]; + } + + public function columnCount(int $argument): int + { + return $this->columns[$argument]; + } + + private function rows(array $arguments): array + { + return array_map( + function ($argument) { + return is_countable($argument) ? count($argument) : 1; + }, + $arguments + ); + } + + private function columns(array $arguments): array + { + return array_map( + function ($argument) { + return is_array($argument) && is_array($argument[array_keys($argument)[0]]) + ? count($argument[array_keys($argument)[0]]) + : 1; + }, + $arguments + ); + } + + public function arrayArguments(): int + { + $count = 0; + foreach (array_keys($this->arguments) as $argument) { + if ($this->rows[$argument] > 1 || $this->columns[$argument] > 1) { + ++$count; + } + } + + return $count; + } + + private function flattenSingleCellArrays(array $arguments, array $rows, array $columns): array + { + foreach ($arguments as $index => $argument) { + if ($rows[$index] === 1 && $columns[$index] === 1) { + while (is_array($argument)) { + $argument = array_pop($argument); + } + $arguments[$index] = $argument; + } + } + + return $arguments; + } + + private function filterArray(array $array): array + { + return array_filter( + $array, + function ($value) { + return $value > 1; + } + ); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php new file mode 100644 index 00000000000..3e69d77ff86 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentProcessor.php @@ -0,0 +1,175 @@ +hasArrayArgument() === false) { + return [$method(...$arguments)]; + } + + if (self::$arrayArgumentHelper->arrayArguments() === 1) { + $nthArgument = self::$arrayArgumentHelper->getFirstArrayArgumentNumber(); + + return self::evaluateNthArgumentAsArray($method, $nthArgument, ...$arguments); + } + + $singleRowVectorIndex = self::$arrayArgumentHelper->getSingleRowVector(); + $singleColumnVectorIndex = self::$arrayArgumentHelper->getSingleColumnVector(); + + if ($singleRowVectorIndex !== null && $singleColumnVectorIndex !== null) { + // Basic logic for a single row vector and a single column vector + return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments); + } + + $matrixPair = self::$arrayArgumentHelper->getMatrixPair(); + if ($matrixPair !== []) { + if ( + (self::$arrayArgumentHelper->isVector($matrixPair[0]) === true && + self::$arrayArgumentHelper->isVector($matrixPair[1]) === false) || + (self::$arrayArgumentHelper->isVector($matrixPair[0]) === false && + self::$arrayArgumentHelper->isVector($matrixPair[1]) === true) + ) { + // Logic for a matrix and a vector (row or column) + return self::evaluateVectorMatrixPair($method, $matrixPair, ...$arguments); + } + // Logic for matrix/matrix, column vector/column vector or row vector/row vector + return self::evaluateMatrixPair($method, $matrixPair, ...$arguments); + } + + // Still need to work out the logic for more than two array arguments, + // For the moment, we're throwing an Exception when we initialise the ArrayArgumentHelper + return ['#VALUE!']; + } + + /** + * @param mixed ...$arguments + */ + private static function evaluateVectorMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array + { + $matrix2 = array_pop($matrixIndexes); + /** @var array $matrixValues2 */ + $matrixValues2 = $arguments[$matrix2]; + $matrix1 = array_pop($matrixIndexes); + /** @var array $matrixValues1 */ + $matrixValues1 = $arguments[$matrix1]; + + $rows = min(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2])); + $columns = min(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2])); + + if ($rows === 1) { + $rows = max(array_map([self::$arrayArgumentHelper, 'rowCount'], [$matrix1, $matrix2])); + } + if ($columns === 1) { + $columns = max(array_map([self::$arrayArgumentHelper, 'columnCount'], [$matrix1, $matrix2])); + } + + $result = []; + for ($rowIndex = 0; $rowIndex < $rows; ++$rowIndex) { + for ($columnIndex = 0; $columnIndex < $columns; ++$columnIndex) { + $rowIndex1 = self::$arrayArgumentHelper->isRowVector($matrix1) ? 0 : $rowIndex; + $columnIndex1 = self::$arrayArgumentHelper->isColumnVector($matrix1) ? 0 : $columnIndex; + $value1 = $matrixValues1[$rowIndex1][$columnIndex1]; + $rowIndex2 = self::$arrayArgumentHelper->isRowVector($matrix2) ? 0 : $rowIndex; + $columnIndex2 = self::$arrayArgumentHelper->isColumnVector($matrix2) ? 0 : $columnIndex; + $value2 = $matrixValues2[$rowIndex2][$columnIndex2]; + $arguments[$matrix1] = $value1; + $arguments[$matrix2] = $value2; + + $result[$rowIndex][$columnIndex] = $method(...$arguments); + } + } + + return $result; + } + + /** + * @param mixed ...$arguments + */ + private static function evaluateMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array + { + $matrix2 = array_pop($matrixIndexes); + /** @var array $matrixValues2 */ + $matrixValues2 = $arguments[$matrix2]; + $matrix1 = array_pop($matrixIndexes); + /** @var array $matrixValues1 */ + $matrixValues1 = $arguments[$matrix1]; + + $result = []; + foreach ($matrixValues1 as $rowIndex => $row) { + foreach ($row as $columnIndex => $value1) { + if (isset($matrixValues2[$rowIndex][$columnIndex]) === false) { + continue; + } + + $value2 = $matrixValues2[$rowIndex][$columnIndex]; + $arguments[$matrix1] = $value1; + $arguments[$matrix2] = $value2; + + $result[$rowIndex][$columnIndex] = $method(...$arguments); + } + } + + return $result; + } + + /** + * @param mixed ...$arguments + */ + private static function evaluateVectorPair(callable $method, int $rowIndex, int $columnIndex, ...$arguments): array + { + $rowVector = Functions::flattenArray($arguments[$rowIndex]); + $columnVector = Functions::flattenArray($arguments[$columnIndex]); + + $result = []; + foreach ($columnVector as $column) { + $rowResults = []; + foreach ($rowVector as $row) { + $arguments[$rowIndex] = $row; + $arguments[$columnIndex] = $column; + + $rowResults[] = $method(...$arguments); + } + $result[] = $rowResults; + } + + return $result; + } + + /** + * Note, offset is from 1 (for the first argument) rather than from 0. + * + * @param mixed ...$arguments + */ + private static function evaluateNthArgumentAsArray(callable $method, int $nthArgument, ...$arguments): array + { + $values = array_slice($arguments, $nthArgument - 1, 1); + /** @var array $values */ + $values = array_pop($values); + + $result = []; + foreach ($values as $value) { + $arguments[$nthArgument - 1] = $value; + $result[] = $method(...$arguments); + } + + return $result; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/BranchPruner.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/BranchPruner.php new file mode 100644 index 00000000000..9cd767e6a6f --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/BranchPruner.php @@ -0,0 +1,223 @@ +branchPruningEnabled = $branchPruningEnabled; + } + + public function clearBranchStore(): void + { + $this->branchStoreKeyCounter = 0; + } + + public function initialiseForLoop(): void + { + $this->currentCondition = null; + $this->currentOnlyIf = null; + $this->currentOnlyIfNot = null; + $this->previousStoreKey = null; + $this->pendingStoreKey = empty($this->storeKeysStack) ? null : end($this->storeKeysStack); + + if ($this->branchPruningEnabled) { + $this->initialiseCondition(); + $this->initialiseThen(); + $this->initialiseElse(); + } + } + + private function initialiseCondition(): void + { + if (isset($this->conditionMap[$this->pendingStoreKey]) && $this->conditionMap[$this->pendingStoreKey]) { + $this->currentCondition = $this->pendingStoreKey; + $stackDepth = count($this->storeKeysStack); + if ($stackDepth > 1) { + // nested if + $this->previousStoreKey = $this->storeKeysStack[$stackDepth - 2]; + } + } + } + + private function initialiseThen(): void + { + if (isset($this->thenMap[$this->pendingStoreKey]) && $this->thenMap[$this->pendingStoreKey]) { + $this->currentOnlyIf = $this->pendingStoreKey; + } elseif ( + isset($this->previousStoreKey, $this->thenMap[$this->previousStoreKey]) + && $this->thenMap[$this->previousStoreKey] + ) { + $this->currentOnlyIf = $this->previousStoreKey; + } + } + + private function initialiseElse(): void + { + if (isset($this->elseMap[$this->pendingStoreKey]) && $this->elseMap[$this->pendingStoreKey]) { + $this->currentOnlyIfNot = $this->pendingStoreKey; + } elseif ( + isset($this->previousStoreKey, $this->elseMap[$this->previousStoreKey]) + && $this->elseMap[$this->previousStoreKey] + ) { + $this->currentOnlyIfNot = $this->previousStoreKey; + } + } + + public function decrementDepth(): void + { + if (!empty($this->pendingStoreKey)) { + --$this->braceDepthMap[$this->pendingStoreKey]; + } + } + + public function incrementDepth(): void + { + if (!empty($this->pendingStoreKey)) { + ++$this->braceDepthMap[$this->pendingStoreKey]; + } + } + + public function functionCall(string $functionName): void + { + if ($this->branchPruningEnabled && ($functionName === 'IF(')) { + // we handle a new if + $this->pendingStoreKey = $this->getUnusedBranchStoreKey(); + $this->storeKeysStack[] = $this->pendingStoreKey; + $this->conditionMap[$this->pendingStoreKey] = true; + $this->braceDepthMap[$this->pendingStoreKey] = 0; + } elseif (!empty($this->pendingStoreKey) && array_key_exists($this->pendingStoreKey, $this->braceDepthMap)) { + // this is not an if but we go deeper + ++$this->braceDepthMap[$this->pendingStoreKey]; + } + } + + public function argumentSeparator(): void + { + if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === 0) { + // We must go to the IF next argument + if ($this->conditionMap[$this->pendingStoreKey]) { + $this->conditionMap[$this->pendingStoreKey] = false; + $this->thenMap[$this->pendingStoreKey] = true; + } elseif ($this->thenMap[$this->pendingStoreKey]) { + $this->thenMap[$this->pendingStoreKey] = false; + $this->elseMap[$this->pendingStoreKey] = true; + } elseif ($this->elseMap[$this->pendingStoreKey]) { + throw new Exception('Reaching fourth argument of an IF'); + } + } + } + + /** + * @param mixed $value + */ + public function closingBrace($value): void + { + if (!empty($this->pendingStoreKey) && $this->braceDepthMap[$this->pendingStoreKey] === -1) { + // we are closing an IF( + if ($value !== 'IF(') { + throw new Exception('Parser bug we should be in an "IF("'); + } + + if ($this->conditionMap[$this->pendingStoreKey]) { + throw new Exception('We should not be expecting a condition'); + } + + $this->thenMap[$this->pendingStoreKey] = false; + $this->elseMap[$this->pendingStoreKey] = false; + --$this->braceDepthMap[$this->pendingStoreKey]; + array_pop($this->storeKeysStack); + $this->pendingStoreKey = null; + } + } + + public function currentCondition(): ?string + { + return $this->currentCondition; + } + + public function currentOnlyIf(): ?string + { + return $this->currentOnlyIf; + } + + public function currentOnlyIfNot(): ?string + { + return $this->currentOnlyIfNot; + } + + private function getUnusedBranchStoreKey(): string + { + $storeKeyValue = 'storeKey-' . $this->branchStoreKeyCounter; + ++$this->branchStoreKeyCounter; + + return $storeKeyValue; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php index 7f4657c43fc..256c3effb1e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engine/Logger.php @@ -87,18 +87,20 @@ class Logger /** * Write an entry to the calculation engine debug log. + * + * @param mixed $args */ - public function writeDebugLog(...$args): void + public function writeDebugLog(string $message, ...$args): void { // Only write the debug log if logging is enabled if ($this->writeDebugLog) { - $message = implode('', $args); + $message = sprintf($message, ...$args); $cellReference = implode(' -> ', $this->cellStack->showStack()); if ($this->echoDebugLog) { echo $cellReference, - ($this->cellStack->count() > 0 ? ' => ' : ''), - $message, - PHP_EOL; + ($this->cellStack->count() > 0 ? ' => ' : ''), + $message, + PHP_EOL; } $this->debugLog[] = $cellReference . ($this->cellStack->count() > 0 ? ' => ' : '') . diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php index a70ddac5cfb..d70b32d6ab0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering.php @@ -61,7 +61,7 @@ class Engineering * If $ord is nonnumeric, BESSELI returns the #VALUE! error value. * If $ord < 0, BESSELI returns the #NUM! error value. * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error */ public static function BESSELI($x, $ord) { @@ -86,7 +86,7 @@ class Engineering * If $ord is nonnumeric, BESSELJ returns the #VALUE! error value. * If $ord < 0, BESSELJ returns the #NUM! error value. * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error */ public static function BESSELJ($x, $ord) { @@ -112,7 +112,7 @@ class Engineering * If $ord is nonnumeric, BESSELK returns the #VALUE! error value. * If $ord < 0, BESSELK returns the #NUM! error value. * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error */ public static function BESSELK($x, $ord) { @@ -137,7 +137,7 @@ class Engineering * If $ord is nonnumeric, BESSELY returns the #VALUE! error value. * If $ord < 0, BESSELY returns the #NUM! error value. * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error */ public static function BESSELY($x, $ord) { @@ -163,7 +163,7 @@ class Engineering * If number is not a valid binary number, or if number contains more than * 10 characters (10 bits), BIN2DEC returns the #NUM! error value. * - * @return string + * @return array|string */ public static function BINTODEC($x) { @@ -195,7 +195,7 @@ class Engineering * If places is nonnumeric, BIN2HEX returns the #VALUE! error value. * If places is negative, BIN2HEX returns the #NUM! error value. * - * @return string + * @return array|string */ public static function BINTOHEX($x, $places = null) { @@ -227,7 +227,7 @@ class Engineering * If places is nonnumeric, BIN2OCT returns the #VALUE! error value. * If places is negative, BIN2OCT returns the #NUM! error value. * - * @return string + * @return array|string */ public static function BINTOOCT($x, $places = null) { @@ -263,7 +263,7 @@ class Engineering * If places is nonnumeric, DEC2BIN returns the #VALUE! error value. * If places is zero or negative, DEC2BIN returns the #NUM! error value. * - * @return string + * @return array|string */ public static function DECTOBIN($x, $places = null) { @@ -299,7 +299,7 @@ class Engineering * If places is nonnumeric, DEC2HEX returns the #VALUE! error value. * If places is zero or negative, DEC2HEX returns the #NUM! error value. * - * @return string + * @return array|string */ public static function DECTOHEX($x, $places = null) { @@ -335,7 +335,7 @@ class Engineering * If places is nonnumeric, DEC2OCT returns the #VALUE! error value. * If places is zero or negative, DEC2OCT returns the #NUM! error value. * - * @return string + * @return array|string */ public static function DECTOOCT($x, $places = null) { @@ -371,7 +371,7 @@ class Engineering * If places is nonnumeric, HEX2BIN returns the #VALUE! error value. * If places is negative, HEX2BIN returns the #NUM! error value. * - * @return string + * @return array|string */ public static function HEXTOBIN($x, $places = null) { @@ -398,7 +398,7 @@ class Engineering * If number is not a valid hexadecimal number, HEX2DEC returns the * #NUM! error value. * - * @return string + * @return array|string */ public static function HEXTODEC($x) { @@ -438,7 +438,7 @@ class Engineering * value. * If places is negative, HEX2OCT returns the #NUM! error value. * - * @return string + * @return array|string */ public static function HEXTOOCT($x, $places = null) { @@ -480,7 +480,7 @@ class Engineering * If places is negative, OCT2BIN returns the #NUM! error * value. * - * @return string + * @return array|string */ public static function OCTTOBIN($x, $places = null) { @@ -507,7 +507,7 @@ class Engineering * If number is not a valid octal number, OCT2DEC returns the * #NUM! error value. * - * @return string + * @return array|string */ public static function OCTTODEC($x) { @@ -544,7 +544,7 @@ class Engineering * If places is nonnumeric, OCT2HEX returns the #VALUE! error value. * If places is negative, OCT2HEX returns the #NUM! error value. * - * @return string + * @return array|string */ public static function OCTTOHEX($x, $places = null) { @@ -563,12 +563,12 @@ class Engineering * * @see Use the COMPLEX() method in the Engineering\Complex class instead * - * @param float $realNumber the real coefficient of the complex number - * @param float $imaginary the imaginary coefficient of the complex number - * @param string $suffix The suffix for the imaginary component of the complex number. + * @param array|float $realNumber the real coefficient of the complex number + * @param array|float $imaginary the imaginary coefficient of the complex number + * @param array|string $suffix The suffix for the imaginary component of the complex number. * If omitted, the suffix is assumed to be "i". * - * @return string + * @return array|string */ public static function COMPLEX($realNumber = 0.0, $imaginary = 0.0, $suffix = 'i') { @@ -590,7 +590,7 @@ class Engineering * @param string $complexNumber the complex number for which you want the imaginary * coefficient * - * @return float|string + * @return array|float|string */ public static function IMAGINARY($complexNumber) { @@ -611,7 +611,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the real coefficient * - * @return float|string + * @return array|float|string */ public static function IMREAL($complexNumber) { @@ -632,7 +632,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the absolute value * - * @return float|string + * @return array|float|string */ public static function IMABS($complexNumber) { @@ -652,9 +652,9 @@ class Engineering * * @see Use the IMARGUMENT() method in the Engineering\ComplexFunctions class instead * - * @param string $complexNumber the complex number for which you want the argument theta + * @param array|string $complexNumber the complex number for which you want the argument theta * - * @return float|string + * @return array|float|string */ public static function IMARGUMENT($complexNumber) { @@ -673,9 +673,9 @@ class Engineering * * @see Use the IMARGUMENT() method in the Engineering\ComplexFunctions class instead * - * @param string $complexNumber the complex number for which you want the conjugate + * @param array|string $complexNumber the complex number for which you want the conjugate * - * @return string + * @return array|string */ public static function IMCONJUGATE($complexNumber) { @@ -694,9 +694,9 @@ class Engineering * * @see Use the IMCOS() method in the Engineering\ComplexFunctions class instead * - * @param string $complexNumber the complex number for which you want the cosine + * @param array|string $complexNumber the complex number for which you want the cosine * - * @return float|string + * @return array|float|string */ public static function IMCOS($complexNumber) { @@ -715,9 +715,9 @@ class Engineering * * @see Use the IMCOSH() method in the Engineering\ComplexFunctions class instead * - * @param string $complexNumber the complex number for which you want the hyperbolic cosine + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosine * - * @return float|string + * @return array|float|string */ public static function IMCOSH($complexNumber) { @@ -736,9 +736,9 @@ class Engineering * * @see Use the IMCOT() method in the Engineering\ComplexFunctions class instead * - * @param string $complexNumber the complex number for which you want the cotangent + * @param array|string $complexNumber the complex number for which you want the cotangent * - * @return float|string + * @return array|float|string */ public static function IMCOT($complexNumber) { @@ -757,9 +757,9 @@ class Engineering * * @see Use the IMCSC() method in the Engineering\ComplexFunctions class instead * - * @param string $complexNumber the complex number for which you want the cosecant + * @param array|string $complexNumber the complex number for which you want the cosecant * - * @return float|string + * @return array|float|string */ public static function IMCSC($complexNumber) { @@ -778,9 +778,9 @@ class Engineering * * @see Use the IMCSCH() method in the Engineering\ComplexFunctions class instead * - * @param string $complexNumber the complex number for which you want the hyperbolic cosecant + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosecant * - * @return float|string + * @return array|float|string */ public static function IMCSCH($complexNumber) { @@ -801,7 +801,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the sine * - * @return float|string + * @return array|float|string */ public static function IMSIN($complexNumber) { @@ -822,7 +822,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the hyperbolic sine * - * @return float|string + * @return array|float|string */ public static function IMSINH($complexNumber) { @@ -843,7 +843,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the secant * - * @return float|string + * @return array|float|string */ public static function IMSEC($complexNumber) { @@ -864,7 +864,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the hyperbolic secant * - * @return float|string + * @return array|float|string */ public static function IMSECH($complexNumber) { @@ -885,7 +885,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the tangent * - * @return float|string + * @return array|float|string */ public static function IMTAN($complexNumber) { @@ -906,7 +906,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the square root * - * @return string + * @return array|string */ public static function IMSQRT($complexNumber) { @@ -927,7 +927,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the natural logarithm * - * @return string + * @return array|string */ public static function IMLN($complexNumber) { @@ -948,7 +948,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the common logarithm * - * @return string + * @return array|string */ public static function IMLOG10($complexNumber) { @@ -969,7 +969,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the base-2 logarithm * - * @return string + * @return array|string */ public static function IMLOG2($complexNumber) { @@ -990,7 +990,7 @@ class Engineering * * @param string $complexNumber the complex number for which you want the exponential * - * @return string + * @return array|string */ public static function IMEXP($complexNumber) { @@ -1012,7 +1012,7 @@ class Engineering * @param string $complexNumber the complex number you want to raise to a power * @param float $realNumber the power to which you want to raise the complex number * - * @return string + * @return array|string */ public static function IMPOWER($complexNumber, $realNumber) { @@ -1034,7 +1034,7 @@ class Engineering * @param string $complexDividend the complex numerator or dividend * @param string $complexDivisor the complex denominator or divisor * - * @return string + * @return array|string */ public static function IMDIV($complexDividend, $complexDivisor) { @@ -1056,7 +1056,7 @@ class Engineering * @param string $complexNumber1 the complex number from which to subtract complexNumber2 * @param string $complexNumber2 the complex number to subtract from complexNumber1 * - * @return string + * @return array|string */ public static function IMSUB($complexNumber1, $complexNumber2) { @@ -1123,7 +1123,7 @@ class Engineering * @param float $a the first number * @param float $b The second number. If omitted, b is assumed to be zero. * - * @return int|string (string in the event of an error) + * @return array|int|string (string in the event of an error) */ public static function DELTA($a, $b = 0) { @@ -1147,7 +1147,7 @@ class Engineering * @param float $number the value to test against step * @param float $step The threshold value. If you omit a value for step, GESTEP uses zero. * - * @return int|string (string in the event of an error) + * @return array|int|string (string in the event of an error) */ public static function GESTEP($number, $step = 0) { @@ -1169,7 +1169,7 @@ class Engineering * @param int $number1 * @param int $number2 * - * @return int|string + * @return array|int|string */ public static function BITAND($number1, $number2) { @@ -1191,7 +1191,7 @@ class Engineering * @param int $number1 * @param int $number2 * - * @return int|string + * @return array|int|string */ public static function BITOR($number1, $number2) { @@ -1213,7 +1213,7 @@ class Engineering * @param int $number1 * @param int $number2 * - * @return int|string + * @return array|int|string */ public static function BITXOR($number1, $number2) { @@ -1235,7 +1235,7 @@ class Engineering * @param int $number * @param int $shiftAmount * - * @return int|string + * @return array|float|int|string */ public static function BITLSHIFT($number, $shiftAmount) { @@ -1257,7 +1257,7 @@ class Engineering * @param int $number * @param int $shiftAmount * - * @return int|string + * @return array|float|int|string */ public static function BITRSHIFT($number, $shiftAmount) { @@ -1285,7 +1285,7 @@ class Engineering * @param float $upper upper bound for integrating ERF. * If omitted, ERF integrates between zero and lower_limit * - * @return float|string + * @return array|float|string */ public static function ERF($lower, $upper = null) { @@ -1306,7 +1306,7 @@ class Engineering * * @param float $limit bound for integrating ERF * - * @return float|string + * @return array|float|string */ public static function ERFPRECISE($limit) { @@ -1332,7 +1332,7 @@ class Engineering * * @param float $x The lower bound for integrating ERFC * - * @return float|string + * @return array|float|string */ public static function ERFC($x) { @@ -1437,7 +1437,7 @@ class Engineering * @param string $fromUOM the units for value * @param string $toUOM the units for the result * - * @return float|string + * @return array|float|string */ public static function CONVERTUOM($value, $fromUOM, $toUOM) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php index ea2577cdf85..2afb52bdb90 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class BesselI { + use ArrayEnabled; + /** * BESSELI. * @@ -21,17 +24,22 @@ class BesselI * * @param mixed $x A float value at which to evaluate the function. * If x is nonnumeric, BESSELI returns the #VALUE! error value. + * Or can be an array of values * @param mixed $ord The integer order of the Bessel function. * If ord is not an integer, it is truncated. * If $ord is nonnumeric, BESSELI returns the #VALUE! error value. * If $ord < 0, BESSELI returns the #NUM! error value. + * Or can be an array of values * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BESSELI($x, $ord) { - $x = Functions::flattenSingleValue($x); - $ord = Functions::flattenSingleValue($ord); + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } try { $x = EngineeringValidations::validateFloat($x); @@ -41,12 +49,12 @@ class BesselI } if ($ord < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $fResult = self::calculate($x, $ord); - return (is_nan($fResult)) ? Functions::NAN() : $fResult; + return (is_nan($fResult)) ? ExcelError::NAN() : $fResult; } private static function calculate(float $x, int $ord): float diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php index 7ea45a74fff..e23b0157d18 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class BesselJ { + use ArrayEnabled; + /** * BESSELJ. * @@ -20,17 +23,22 @@ class BesselJ * * @param mixed $x A float value at which to evaluate the function. * If x is nonnumeric, BESSELJ returns the #VALUE! error value. + * Or can be an array of values * @param mixed $ord The integer order of the Bessel function. * If ord is not an integer, it is truncated. * If $ord is nonnumeric, BESSELJ returns the #VALUE! error value. * If $ord < 0, BESSELJ returns the #NUM! error value. + * Or can be an array of values * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BESSELJ($x, $ord) { - $x = Functions::flattenSingleValue($x); - $ord = Functions::flattenSingleValue($ord); + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } try { $x = EngineeringValidations::validateFloat($x); @@ -40,12 +48,12 @@ class BesselJ } if ($ord < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $fResult = self::calculate($x, $ord); - return (is_nan($fResult)) ? Functions::NAN() : $fResult; + return (is_nan($fResult)) ? ExcelError::NAN() : $fResult; } private static function calculate(float $x, int $ord): float diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php index 8facdffc79f..2d21e7529af 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class BesselK { + use ArrayEnabled; + /** * BESSELK. * @@ -18,17 +22,22 @@ class BesselK * * @param mixed $x A float value at which to evaluate the function. * If x is nonnumeric, BESSELK returns the #VALUE! error value. + * Or can be an array of values * @param mixed $ord The integer order of the Bessel function. * If ord is not an integer, it is truncated. * If $ord is nonnumeric, BESSELK returns the #VALUE! error value. * If $ord < 0, BESSELKI returns the #NUM! error value. + * Or can be an array of values * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BESSELK($x, $ord) { - $x = Functions::flattenSingleValue($x); - $ord = Functions::flattenSingleValue($ord); + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } try { $x = EngineeringValidations::validateFloat($x); @@ -38,12 +47,12 @@ class BesselK } if (($ord < 0) || ($x <= 0.0)) { - return Functions::NAN(); + return ExcelError::NAN(); } $fBk = self::calculate($x, $ord); - return (is_nan($fBk)) ? Functions::NAN() : $fBk; + return (is_nan($fBk)) ? ExcelError::NAN() : $fBk; } private static function calculate(float $x, int $ord): float @@ -59,13 +68,28 @@ class BesselK return self::besselK2($x, $ord); } + /** + * Mollify Phpstan. + * + * @codeCoverageIgnore + */ + private static function callBesselI(float $x, int $ord): float + { + $rslt = BesselI::BESSELI($x, $ord); + if (!is_float($rslt)) { + throw new Exception('Unexpected array or string'); + } + + return $rslt; + } + private static function besselK0(float $x): float { if ($x <= 2) { $fNum2 = $x * 0.5; $y = ($fNum2 * $fNum2); - return -log($fNum2) * BesselI::BESSELI($x, 0) + + return -log($fNum2) * self::callBesselI($x, 0) + (-0.57721566 + $y * (0.42278420 + $y * (0.23069756 + $y * (0.3488590e-1 + $y * (0.262698e-2 + $y * (0.10750e-3 + $y * 0.74e-5)))))); } @@ -83,7 +107,7 @@ class BesselK $fNum2 = $x * 0.5; $y = ($fNum2 * $fNum2); - return log($fNum2) * BesselI::BESSELI($x, 1) + + return log($fNum2) * self::callBesselI($x, 1) + (1 + $y * (0.15443144 + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1 + $y * (-0.110404e-2 + $y * (-0.4686e-4))))))) / $x; } @@ -95,7 +119,7 @@ class BesselK (0.325614e-2 + $y * (-0.68245e-3))))))); } - private static function besselK2(float $x, int $ord) + private static function besselK2(float $x, int $ord): float { $fTox = 2 / $x; $fBkm = self::besselK0($x); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php index 7f387497169..31d9694a706 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class BesselY { + use ArrayEnabled; + /** * BESSELY. * @@ -17,17 +20,22 @@ class BesselY * * @param mixed $x A float value at which to evaluate the function. * If x is nonnumeric, BESSELY returns the #VALUE! error value. + * Or can be an array of values * @param mixed $ord The integer order of the Bessel function. * If ord is not an integer, it is truncated. * If $ord is nonnumeric, BESSELY returns the #VALUE! error value. * If $ord < 0, BESSELY returns the #NUM! error value. + * Or can be an array of values * - * @return float|string Result, or a string containing an error + * @return array|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BESSELY($x, $ord) { - $x = Functions::flattenSingleValue($x); - $ord = Functions::flattenSingleValue($ord); + if (is_array($x) || is_array($ord)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $ord); + } try { $x = EngineeringValidations::validateFloat($x); @@ -37,12 +45,12 @@ class BesselY } if (($ord < 0) || ($x <= 0.0)) { - return Functions::NAN(); + return ExcelError::NAN(); } $fBy = self::calculate($x, $ord); - return (is_nan($fBy)) ? Functions::NAN() : $fBy; + return (is_nan($fBy)) ? ExcelError::NAN() : $fBy; } private static function calculate(float $x, int $ord): float @@ -58,6 +66,21 @@ class BesselY return self::besselY2($x, $ord); } + /** + * Mollify Phpstan. + * + * @codeCoverageIgnore + */ + private static function callBesselJ(float $x, int $ord): float + { + $rslt = BesselJ::BESSELJ($x, $ord); + if (!is_float($rslt)) { + throw new Exception('Unexpected array or string'); + } + + return $rslt; + } + private static function besselY0(float $x): float { if ($x < 8.0) { @@ -67,7 +90,7 @@ class BesselY $ans2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y * (47447.26470 + $y * (226.1030244 + $y)))); - return $ans1 / $ans2 + 0.636619772 * BesselJ::BESSELJ($x, 0) * log($x); + return $ans1 / $ans2 + 0.636619772 * self::callBesselJ($x, 0) * log($x); } $z = 8.0 / $x; @@ -89,7 +112,7 @@ class BesselY $ans2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y * (0.1020426050e6 + $y * (0.3549632885e3 + $y))))); - return ($ans1 / $ans2) + 0.636619772 * (BesselJ::BESSELJ($x, 1) * log($x) - 1 / $x); + return ($ans1 / $ans2) + 0.636619772 * (self::callBesselJ($x, 1) * log($x) - 1 / $x); } $z = 8.0 / $x; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php index 9958f054f2d..adeb1b5535e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class BitWise { + use ArrayEnabled; + const SPLIT_DIVISOR = 2 ** 24; /** @@ -27,13 +31,21 @@ class BitWise * Excel Function: * BITAND(number1, number2) * - * @param int $number1 - * @param int $number2 + * @param array|int $number1 + * Or can be an array of values + * @param array|int $number2 + * Or can be an array of values * - * @return int|string + * @return array|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BITAND($number1, $number2) { + if (is_array($number1) || is_array($number2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); + } + try { $number1 = self::validateBitwiseArgument($number1); $number2 = self::validateBitwiseArgument($number2); @@ -54,13 +66,21 @@ class BitWise * Excel Function: * BITOR(number1, number2) * - * @param int $number1 - * @param int $number2 + * @param array|int $number1 + * Or can be an array of values + * @param array|int $number2 + * Or can be an array of values * - * @return int|string + * @return array|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BITOR($number1, $number2) { + if (is_array($number1) || is_array($number2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); + } + try { $number1 = self::validateBitwiseArgument($number1); $number2 = self::validateBitwiseArgument($number2); @@ -82,13 +102,21 @@ class BitWise * Excel Function: * BITXOR(number1, number2) * - * @param int $number1 - * @param int $number2 + * @param array|int $number1 + * Or can be an array of values + * @param array|int $number2 + * Or can be an array of values * - * @return int|string + * @return array|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BITXOR($number1, $number2) { + if (is_array($number1) || is_array($number2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number1, $number2); + } + try { $number1 = self::validateBitwiseArgument($number1); $number2 = self::validateBitwiseArgument($number2); @@ -110,13 +138,21 @@ class BitWise * Excel Function: * BITLSHIFT(number, shift_amount) * - * @param int $number - * @param int $shiftAmount + * @param array|int $number + * Or can be an array of values + * @param array|int $shiftAmount + * Or can be an array of values * - * @return float|int|string + * @return array|float|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BITLSHIFT($number, $shiftAmount) { + if (is_array($number) || is_array($shiftAmount)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount); + } + try { $number = self::validateBitwiseArgument($number); $shiftAmount = self::validateShiftAmount($shiftAmount); @@ -126,7 +162,7 @@ class BitWise $result = floor($number * (2 ** $shiftAmount)); if ($result > 2 ** 48 - 1) { - return Functions::NAN(); + return ExcelError::NAN(); } return $result; @@ -140,13 +176,21 @@ class BitWise * Excel Function: * BITRSHIFT(number, shift_amount) * - * @param int $number - * @param int $shiftAmount + * @param array|int $number + * Or can be an array of values + * @param array|int $shiftAmount + * Or can be an array of values * - * @return float|int|string + * @return array|float|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function BITRSHIFT($number, $shiftAmount) { + if (is_array($number) || is_array($shiftAmount)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $shiftAmount); + } + try { $number = self::validateBitwiseArgument($number); $shiftAmount = self::validateShiftAmount($shiftAmount); @@ -156,7 +200,7 @@ class BitWise $result = floor($number / (2 ** $shiftAmount)); if ($result > 2 ** 48 - 1) { // possible because shiftAmount can be negative - return Functions::NAN(); + return ExcelError::NAN(); } return $result; @@ -167,25 +211,26 @@ class BitWise * * @param mixed $value * - * @return float|int + * @return float */ private static function validateBitwiseArgument($value) { - self::nullFalseTrueToNumber($value); + $value = self::nullFalseTrueToNumber($value); if (is_numeric($value)) { + $value = (float) $value; if ($value == floor($value)) { if (($value > 2 ** 48 - 1) || ($value < 0)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return floor($value); } - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } /** @@ -197,31 +242,34 @@ class BitWise */ private static function validateShiftAmount($value) { - self::nullFalseTrueToNumber($value); + $value = self::nullFalseTrueToNumber($value); if (is_numeric($value)) { if (abs($value) > 53) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return (int) $value; } - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } /** * Many functions accept null/false/true argument treated as 0/0/1. * * @param mixed $number + * + * @return mixed */ - public static function nullFalseTrueToNumber(&$number): void + private static function nullFalseTrueToNumber(&$number) { - $number = Functions::flattenSingleValue($number); if ($number === null) { $number = 0; } elseif (is_bool($number)) { $number = (int) $number; } + + return $number; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Compare.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Compare.php index 0a6342069c9..6aaf1faa5b5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Compare.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Compare.php @@ -2,11 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Compare { + use ArrayEnabled; + /** * DELTA. * @@ -18,15 +20,20 @@ class Compare * functions you calculate the count of equal pairs. This function is also known as the * Kronecker Delta function. * - * @param float $a the first number - * @param float $b The second number. If omitted, b is assumed to be zero. + * @param array|float $a the first number + * Or can be an array of values + * @param array|float $b The second number. If omitted, b is assumed to be zero. + * Or can be an array of values * - * @return int|string (string in the event of an error) + * @return array|int|string (string in the event of an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function DELTA($a, $b = 0) + public static function DELTA($a, $b = 0.0) { - $a = Functions::flattenSingleValue($a); - $b = Functions::flattenSingleValue($b); + if (is_array($a) || is_array($b)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $a, $b); + } try { $a = EngineeringValidations::validateFloat($a); @@ -35,7 +42,7 @@ class Compare return $e->getMessage(); } - return (int) ($a == $b); + return (int) (abs($a - $b) < 1.0e-15); } /** @@ -48,19 +55,24 @@ class Compare * Use this function to filter a set of values. For example, by summing several GESTEP * functions you calculate the count of values that exceed a threshold. * - * @param float $number the value to test against step - * @param float $step The threshold value. If you omit a value for step, GESTEP uses zero. + * @param array|float $number the value to test against step + * Or can be an array of values + * @param null|array|float $step The threshold value. If you omit a value for step, GESTEP uses zero. + * Or can be an array of values * - * @return int|string (string in the event of an error) + * @return array|int|string (string in the event of an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function GESTEP($number, $step = 0) + public static function GESTEP($number, $step = 0.0) { - $number = Functions::flattenSingleValue($number); - $step = Functions::flattenSingleValue($step); + if (is_array($number) || is_array($step)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $step); + } try { $number = EngineeringValidations::validateFloat($number); - $step = EngineeringValidations::validateFloat($step); + $step = EngineeringValidations::validateFloat($step ?? 0.0); } catch (Exception $e) { return $e->getMessage(); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Complex.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Complex.php index 1c2f5f773ac..691de8b704f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Complex.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Complex.php @@ -4,11 +4,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use Complex\Complex as ComplexObject; use Complex\Exception as ComplexException; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Complex { + use ArrayEnabled; + /** * COMPLEX. * @@ -18,17 +21,26 @@ class Complex * COMPLEX(realNumber,imaginary[,suffix]) * * @param mixed $realNumber the real float coefficient of the complex number + * Or can be an array of values * @param mixed $imaginary the imaginary float coefficient of the complex number + * Or can be an array of values * @param mixed $suffix The character suffix for the imaginary component of the complex number. * If omitted, the suffix is assumed to be "i". + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function COMPLEX($realNumber = 0.0, $imaginary = 0.0, $suffix = 'i') { - $realNumber = ($realNumber === null) ? 0.0 : Functions::flattenSingleValue($realNumber); - $imaginary = ($imaginary === null) ? 0.0 : Functions::flattenSingleValue($imaginary); - $suffix = ($suffix === null) ? 'i' : Functions::flattenSingleValue($suffix); + if (is_array($realNumber) || is_array($imaginary) || is_array($suffix)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $realNumber, $imaginary, $suffix); + } + + $realNumber = $realNumber ?? 0.0; + $imaginary = $imaginary ?? 0.0; + $suffix = $suffix ?? 'i'; try { $realNumber = EngineeringValidations::validateFloat($realNumber); @@ -43,7 +55,7 @@ class Complex return (string) $complex; } - return Functions::VALUE(); + return ExcelError::VALUE(); } /** @@ -54,19 +66,24 @@ class Complex * Excel Function: * IMAGINARY(complexNumber) * - * @param string $complexNumber the complex number for which you want the imaginary + * @param array|string $complexNumber the complex number for which you want the imaginary * coefficient + * Or can be an array of values * - * @return float|string + * @return array|float|string (string if an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMAGINARY($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return $complex->getImaginary(); @@ -80,18 +97,23 @@ class Complex * Excel Function: * IMREAL(complexNumber) * - * @param string $complexNumber the complex number for which you want the real coefficient + * @param array|string $complexNumber the complex number for which you want the real coefficient + * Or can be an array of values * - * @return float|string + * @return array|float|string (string if an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMREAL($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return $complex->getReal(); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php index 3f37f373ff7..28a27a0317e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php @@ -4,10 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use Complex\Complex as ComplexObject; use Complex\Exception as ComplexException; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ComplexFunctions { + use ArrayEnabled; + /** * IMABS. * @@ -16,18 +19,23 @@ class ComplexFunctions * Excel Function: * IMABS(complexNumber) * - * @param string $complexNumber the complex number for which you want the absolute value + * @param array|string $complexNumber the complex number for which you want the absolute value + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMABS($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return $complex->abs(); @@ -42,22 +50,27 @@ class ComplexFunctions * Excel Function: * IMARGUMENT(complexNumber) * - * @param string $complexNumber the complex number for which you want the argument theta + * @param array|string $complexNumber the complex number for which you want the argument theta + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMARGUMENT($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } return $complex->argument(); @@ -71,18 +84,23 @@ class ComplexFunctions * Excel Function: * IMCONJUGATE(complexNumber) * - * @param string $complexNumber the complex number for which you want the conjugate + * @param array|string $complexNumber the complex number for which you want the conjugate + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMCONJUGATE($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->conjugate(); @@ -96,18 +114,23 @@ class ComplexFunctions * Excel Function: * IMCOS(complexNumber) * - * @param string $complexNumber the complex number for which you want the cosine + * @param array|string $complexNumber the complex number for which you want the cosine + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMCOS($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->cos(); @@ -121,18 +144,23 @@ class ComplexFunctions * Excel Function: * IMCOSH(complexNumber) * - * @param string $complexNumber the complex number for which you want the hyperbolic cosine + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosine + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMCOSH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->cosh(); @@ -146,18 +174,23 @@ class ComplexFunctions * Excel Function: * IMCOT(complexNumber) * - * @param string $complexNumber the complex number for which you want the cotangent + * @param array|string $complexNumber the complex number for which you want the cotangent + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMCOT($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->cot(); @@ -171,18 +204,23 @@ class ComplexFunctions * Excel Function: * IMCSC(complexNumber) * - * @param string $complexNumber the complex number for which you want the cosecant + * @param array|string $complexNumber the complex number for which you want the cosecant + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMCSC($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->csc(); @@ -196,18 +234,23 @@ class ComplexFunctions * Excel Function: * IMCSCH(complexNumber) * - * @param string $complexNumber the complex number for which you want the hyperbolic cosecant + * @param array|string $complexNumber the complex number for which you want the hyperbolic cosecant + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMCSCH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->csch(); @@ -221,18 +264,23 @@ class ComplexFunctions * Excel Function: * IMSIN(complexNumber) * - * @param string $complexNumber the complex number for which you want the sine + * @param array|string $complexNumber the complex number for which you want the sine + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMSIN($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->sin(); @@ -246,18 +294,23 @@ class ComplexFunctions * Excel Function: * IMSINH(complexNumber) * - * @param string $complexNumber the complex number for which you want the hyperbolic sine + * @param array|string $complexNumber the complex number for which you want the hyperbolic sine + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMSINH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->sinh(); @@ -271,18 +324,23 @@ class ComplexFunctions * Excel Function: * IMSEC(complexNumber) * - * @param string $complexNumber the complex number for which you want the secant + * @param array|string $complexNumber the complex number for which you want the secant + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMSEC($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->sec(); @@ -296,18 +354,23 @@ class ComplexFunctions * Excel Function: * IMSECH(complexNumber) * - * @param string $complexNumber the complex number for which you want the hyperbolic secant + * @param array|string $complexNumber the complex number for which you want the hyperbolic secant + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMSECH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->sech(); @@ -321,18 +384,23 @@ class ComplexFunctions * Excel Function: * IMTAN(complexNumber) * - * @param string $complexNumber the complex number for which you want the tangent + * @param array|string $complexNumber the complex number for which you want the tangent + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMTAN($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->tan(); @@ -346,22 +414,27 @@ class ComplexFunctions * Excel Function: * IMSQRT(complexNumber) * - * @param string $complexNumber the complex number for which you want the square root + * @param array|string $complexNumber the complex number for which you want the square root + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMSQRT($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } $theta = self::IMARGUMENT($complexNumber); - if ($theta === Functions::DIV0()) { + if ($theta === ExcelError::DIV0()) { return '0'; } @@ -376,22 +449,27 @@ class ComplexFunctions * Excel Function: * IMLN(complexNumber) * - * @param string $complexNumber the complex number for which you want the natural logarithm + * @param array|string $complexNumber the complex number for which you want the natural logarithm + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMLN($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->ln(); @@ -405,22 +483,27 @@ class ComplexFunctions * Excel Function: * IMLOG10(complexNumber) * - * @param string $complexNumber the complex number for which you want the common logarithm + * @param array|string $complexNumber the complex number for which you want the common logarithm + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMLOG10($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->log10(); @@ -434,22 +517,27 @@ class ComplexFunctions * Excel Function: * IMLOG2(complexNumber) * - * @param string $complexNumber the complex number for which you want the base-2 logarithm + * @param array|string $complexNumber the complex number for which you want the base-2 logarithm + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMLOG2($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->log2(); @@ -463,18 +551,23 @@ class ComplexFunctions * Excel Function: * IMEXP(complexNumber) * - * @param string $complexNumber the complex number for which you want the exponential + * @param array|string $complexNumber the complex number for which you want the exponential + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMEXP($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); + if (is_array($complexNumber)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $complexNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $complex->exp(); @@ -488,26 +581,31 @@ class ComplexFunctions * Excel Function: * IMPOWER(complexNumber,realNumber) * - * @param string $complexNumber the complex number you want to raise to a power - * @param float $realNumber the power to which you want to raise the complex number + * @param array|string $complexNumber the complex number you want to raise to a power + * Or can be an array of values + * @param array|float|int|string $realNumber the power to which you want to raise the complex number + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMPOWER($complexNumber, $realNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - $realNumber = Functions::flattenSingleValue($realNumber); + if (is_array($complexNumber) || is_array($realNumber)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber, $realNumber); + } try { $complex = new ComplexObject($complexNumber); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } if (!is_numeric($realNumber)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } - return (string) $complex->pow($realNumber); + return (string) $complex->pow((float) $realNumber); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php index 681aad8cafe..e525b4b9aa5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php @@ -4,10 +4,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use Complex\Complex as ComplexObject; use Complex\Exception as ComplexException; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ComplexOperations { + use ArrayEnabled; + /** * IMDIV. * @@ -16,20 +20,25 @@ class ComplexOperations * Excel Function: * IMDIV(complexDividend,complexDivisor) * - * @param string $complexDividend the complex numerator or dividend - * @param string $complexDivisor the complex denominator or divisor + * @param array|string $complexDividend the complex numerator or dividend + * Or can be an array of values + * @param array|string $complexDivisor the complex denominator or divisor + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMDIV($complexDividend, $complexDivisor) { - $complexDividend = Functions::flattenSingleValue($complexDividend); - $complexDivisor = Functions::flattenSingleValue($complexDivisor); + if (is_array($complexDividend) || is_array($complexDivisor)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexDividend, $complexDivisor); + } try { return (string) (new ComplexObject($complexDividend))->divideby(new ComplexObject($complexDivisor)); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } } @@ -41,20 +50,25 @@ class ComplexOperations * Excel Function: * IMSUB(complexNumber1,complexNumber2) * - * @param string $complexNumber1 the complex number from which to subtract complexNumber2 - * @param string $complexNumber2 the complex number to subtract from complexNumber1 + * @param array|string $complexNumber1 the complex number from which to subtract complexNumber2 + * Or can be an array of values + * @param array|string $complexNumber2 the complex number to subtract from complexNumber1 + * Or can be an array of values * - * @return string + * @return array|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function IMSUB($complexNumber1, $complexNumber2) { - $complexNumber1 = Functions::flattenSingleValue($complexNumber1); - $complexNumber2 = Functions::flattenSingleValue($complexNumber2); + if (is_array($complexNumber1) || is_array($complexNumber2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $complexNumber1, $complexNumber2); + } try { return (string) (new ComplexObject($complexNumber1))->subtract(new ComplexObject($complexNumber2)); } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } } @@ -82,7 +96,7 @@ class ComplexOperations $returnValue = $returnValue->add(new ComplexObject($complex)); } } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $returnValue; @@ -112,7 +126,7 @@ class ComplexOperations $returnValue = $returnValue->multiply(new ComplexObject($complex)); } } catch (ComplexException $e) { - return Functions::NAN(); + return ExcelError::NAN(); } return (string) $returnValue; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php index a095690d18c..e80490bd1fb 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php @@ -2,16 +2,20 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; -class ConvertBase +abstract class ConvertBase { + use ArrayEnabled; + protected static function validateValue($value): string { if (is_bool($value)) { if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } $value = (int) $value; } @@ -33,13 +37,13 @@ class ConvertBase if (is_numeric($places)) { if ($places < 0 || $places > 10) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return (int) $places; } - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } /** @@ -57,7 +61,7 @@ class ConvertBase return substr(str_pad($value, $places, '0', STR_PAD_LEFT), -10); } - return Functions::NAN(); + return ExcelError::NAN(); } return substr($value, -10); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php index a662b78de30..4741f301008 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ConvertBinary extends ConvertBase { @@ -15,17 +15,26 @@ class ConvertBinary extends ConvertBase * Excel Function: * BIN2DEC(x) * - * @param string $value The binary number (as a string) that you want to convert. The number + * @param array|string $value The binary number (as a string) that you want to convert. The number * cannot contain more than 10 characters (10 bits). The most significant * bit of number is the sign bit. The remaining 9 bits are magnitude bits. * Negative numbers are represented using two's-complement notation. * If number is not a valid binary number, or if number contains more than * 10 characters (10 bits), BIN2DEC returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toDecimal($value): string + public static function toDecimal($value) { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateBinary($value); } catch (Exception $e) { return $e->getMessage(); @@ -49,25 +58,35 @@ class ConvertBinary extends ConvertBase * Excel Function: * BIN2HEX(x[,places]) * - * @param string $value The binary number (as a string) that you want to convert. The number + * @param array|string $value The binary number (as a string) that you want to convert. The number * cannot contain more than 10 characters (10 bits). The most significant * bit of number is the sign bit. The remaining 9 bits are magnitude bits. * Negative numbers are represented using two's-complement notation. * If number is not a valid binary number, or if number contains more than * 10 characters (10 bits), BIN2HEX returns the #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, BIN2HEX uses the + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, BIN2HEX uses the * minimum number of characters necessary. Places is useful for padding the * return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, BIN2HEX returns the #VALUE! error value. * If places is negative, BIN2HEX returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toHex($value, $places = null): string + public static function toHex($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateBinary($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } @@ -92,25 +111,35 @@ class ConvertBinary extends ConvertBase * Excel Function: * BIN2OCT(x[,places]) * - * @param string $value The binary number (as a string) that you want to convert. The number + * @param array|string $value The binary number (as a string) that you want to convert. The number * cannot contain more than 10 characters (10 bits). The most significant * bit of number is the sign bit. The remaining 9 bits are magnitude bits. * Negative numbers are represented using two's-complement notation. * If number is not a valid binary number, or if number contains more than * 10 characters (10 bits), BIN2OCT returns the #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, BIN2OCT uses the + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, BIN2OCT uses the * minimum number of characters necessary. Places is useful for padding the * return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, BIN2OCT returns the #VALUE! error value. * If places is negative, BIN2OCT returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toOctal($value, $places = null): string + public static function toOctal($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateBinary($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } @@ -126,7 +155,7 @@ class ConvertBinary extends ConvertBase protected static function validateBinary(string $value): string { if ((strlen($value) > preg_match_all('/[01]/', $value)) || (strlen($value) > 10)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php index a34332fb353..9b59d3995f0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ConvertDecimal extends ConvertBase { @@ -22,7 +22,7 @@ class ConvertDecimal extends ConvertBase * Excel Function: * DEC2BIN(x[,places]) * - * @param string $value The decimal integer you want to convert. If number is negative, + * @param array|string $value The decimal integer you want to convert. If number is negative, * valid place values are ignored and DEC2BIN returns a 10-character * (10-bit) binary number in which the most significant bit is the sign * bit. The remaining 9 bits are magnitude bits. Negative numbers are @@ -32,26 +32,36 @@ class ConvertDecimal extends ConvertBase * If number is nonnumeric, DEC2BIN returns the #VALUE! error value. * If DEC2BIN requires more than places characters, it returns the #NUM! * error value. - * @param int $places The number of characters to use. If places is omitted, DEC2BIN uses + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, DEC2BIN uses * the minimum number of characters necessary. Places is useful for * padding the return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, DEC2BIN returns the #VALUE! error value. * If places is zero or negative, DEC2BIN returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toBinary($value, $places = null): string + public static function toBinary($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateDecimal($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } $value = (int) floor((float) $value); if ($value > self::LARGEST_BINARY_IN_DECIMAL || $value < self::SMALLEST_BINARY_IN_DECIMAL) { - return Functions::NAN(); + return ExcelError::NAN(); } $r = decbin($value); @@ -69,7 +79,7 @@ class ConvertDecimal extends ConvertBase * Excel Function: * DEC2HEX(x[,places]) * - * @param string $value The decimal integer you want to convert. If number is negative, + * @param array|string $value The decimal integer you want to convert. If number is negative, * places is ignored and DEC2HEX returns a 10-character (40-bit) * hexadecimal number in which the most significant bit is the sign * bit. The remaining 39 bits are magnitude bits. Negative numbers @@ -79,26 +89,36 @@ class ConvertDecimal extends ConvertBase * If number is nonnumeric, DEC2HEX returns the #VALUE! error value. * If DEC2HEX requires more than places characters, it returns the * #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, DEC2HEX uses + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, DEC2HEX uses * the minimum number of characters necessary. Places is useful for * padding the return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, DEC2HEX returns the #VALUE! error value. * If places is zero or negative, DEC2HEX returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toHex($value, $places = null): string + public static function toHex($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateDecimal($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } $value = floor((float) $value); if ($value > self::LARGEST_HEX_IN_DECIMAL || $value < self::SMALLEST_HEX_IN_DECIMAL) { - return Functions::NAN(); + return ExcelError::NAN(); } $r = strtoupper(dechex((int) $value)); $r = self::hex32bit($value, $r); @@ -135,7 +155,7 @@ class ConvertDecimal extends ConvertBase * Excel Function: * DEC2OCT(x[,places]) * - * @param string $value The decimal integer you want to convert. If number is negative, + * @param array|string $value The decimal integer you want to convert. If number is negative, * places is ignored and DEC2OCT returns a 10-character (30-bit) * octal number in which the most significant bit is the sign bit. * The remaining 29 bits are magnitude bits. Negative numbers are @@ -145,26 +165,36 @@ class ConvertDecimal extends ConvertBase * If number is nonnumeric, DEC2OCT returns the #VALUE! error value. * If DEC2OCT requires more than places characters, it returns the * #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, DEC2OCT uses + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, DEC2OCT uses * the minimum number of characters necessary. Places is useful for * padding the return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, DEC2OCT returns the #VALUE! error value. * If places is zero or negative, DEC2OCT returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toOctal($value, $places = null): string + public static function toOctal($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateDecimal($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } $value = (int) floor((float) $value); if ($value > self::LARGEST_OCTAL_IN_DECIMAL || $value < self::SMALLEST_OCTAL_IN_DECIMAL) { - return Functions::NAN(); + return ExcelError::NAN(); } $r = decoct($value); $r = substr($r, -10); @@ -175,7 +205,7 @@ class ConvertDecimal extends ConvertBase protected static function validateDecimal(string $value): string { if (strlen($value) > preg_match_all('/[-0123456789.]/', $value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php index de1b0704f78..55ce209ecd5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ConvertHex extends ConvertBase { @@ -15,7 +15,7 @@ class ConvertHex extends ConvertBase * Excel Function: * HEX2BIN(x[,places]) * - * @param string $value The hexadecimal number you want to convert. + * @param array|string $value The hexadecimal number you want to convert. * Number cannot contain more than 10 characters. * The most significant bit of number is the sign bit (40th bit from the right). * The remaining 9 bits are magnitude bits. @@ -25,19 +25,29 @@ class ConvertHex extends ConvertBase * and if number is positive, it cannot be greater than 1FF. * If number is not a valid hexadecimal number, HEX2BIN returns the #NUM! error value. * If HEX2BIN requires more than places characters, it returns the #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, * HEX2BIN uses the minimum number of characters necessary. Places * is useful for padding the return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, HEX2BIN returns the #VALUE! error value. * If places is negative, HEX2BIN returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toBinary($value, $places = null): string + public static function toBinary($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateHex($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } @@ -55,25 +65,34 @@ class ConvertHex extends ConvertBase * Excel Function: * HEX2DEC(x) * - * @param string $value The hexadecimal number you want to convert. This number cannot + * @param array|string $value The hexadecimal number you want to convert. This number cannot * contain more than 10 characters (40 bits). The most significant * bit of number is the sign bit. The remaining 39 bits are magnitude * bits. Negative numbers are represented using two's-complement * notation. * If number is not a valid hexadecimal number, HEX2DEC returns the * #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toDecimal($value): string + public static function toDecimal($value) { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateHex($value); } catch (Exception $e) { return $e->getMessage(); } if (strlen($value) > 10) { - return Functions::NAN(); + return ExcelError::NAN(); } $binX = ''; @@ -99,7 +118,7 @@ class ConvertHex extends ConvertBase * Excel Function: * HEX2OCT(x[,places]) * - * @param string $value The hexadecimal number you want to convert. Number cannot + * @param array|string $value The hexadecimal number you want to convert. Number cannot * contain more than 10 characters. The most significant bit of * number is the sign bit. The remaining 39 bits are magnitude * bits. Negative numbers are represented using two's-complement @@ -112,20 +131,30 @@ class ConvertHex extends ConvertBase * the #NUM! error value. * If HEX2OCT requires more than places characters, it returns * the #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, HEX2OCT + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, HEX2OCT * uses the minimum number of characters necessary. Places is * useful for padding the return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, HEX2OCT returns the #VALUE! error * value. * If places is negative, HEX2OCT returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toOctal($value, $places = null): string + public static function toOctal($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateHex($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } @@ -138,7 +167,7 @@ class ConvertHex extends ConvertBase protected static function validateHex(string $value): string { if (strlen($value) > preg_match_all('/[0123456789ABCDEF]/', $value)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php index 1181e2ee191..add7aba01b9 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ConvertOctal extends ConvertBase { @@ -15,7 +15,7 @@ class ConvertOctal extends ConvertBase * Excel Function: * OCT2BIN(x[,places]) * - * @param string $value The octal number you want to convert. Number may not + * @param array|string $value The octal number you want to convert. Number may not * contain more than 10 characters. The most significant * bit of number is the sign bit. The remaining 29 bits * are magnitude bits. Negative numbers are represented @@ -28,7 +28,8 @@ class ConvertOctal extends ConvertBase * the #NUM! error value. * If OCT2BIN requires more than places characters, it * returns the #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, * OCT2BIN uses the minimum number of characters necessary. * Places is useful for padding the return value with * leading 0s (zeros). @@ -37,13 +38,22 @@ class ConvertOctal extends ConvertBase * error value. * If places is negative, OCT2BIN returns the #NUM! error * value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toBinary($value, $places = null): string + public static function toBinary($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateOctal($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } @@ -59,18 +69,27 @@ class ConvertOctal extends ConvertBase * Excel Function: * OCT2DEC(x) * - * @param string $value The octal number you want to convert. Number may not contain + * @param array|string $value The octal number you want to convert. Number may not contain * more than 10 octal characters (30 bits). The most significant * bit of number is the sign bit. The remaining 29 bits are * magnitude bits. Negative numbers are represented using * two's-complement notation. * If number is not a valid octal number, OCT2DEC returns the * #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toDecimal($value): string + public static function toDecimal($value) { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateOctal($value); } catch (Exception $e) { return $e->getMessage(); @@ -99,7 +118,7 @@ class ConvertOctal extends ConvertBase * Excel Function: * OCT2HEX(x[,places]) * - * @param string $value The octal number you want to convert. Number may not contain + * @param array|string $value The octal number you want to convert. Number may not contain * more than 10 octal characters (30 bits). The most significant * bit of number is the sign bit. The remaining 29 bits are * magnitude bits. Negative numbers are represented using @@ -110,25 +129,35 @@ class ConvertOctal extends ConvertBase * #NUM! error value. * If OCT2HEX requires more than places characters, it returns * the #NUM! error value. - * @param int $places The number of characters to use. If places is omitted, OCT2HEX + * Or can be an array of values + * @param array|int $places The number of characters to use. If places is omitted, OCT2HEX * uses the minimum number of characters necessary. Places is useful * for padding the return value with leading 0s (zeros). * If places is not an integer, it is truncated. * If places is nonnumeric, OCT2HEX returns the #VALUE! error value. * If places is negative, OCT2HEX returns the #NUM! error value. + * Or can be an array of values + * + * @return array|string Result, or an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ - public static function toHex($value, $places = null): string + public static function toHex($value, $places = null) { + if (is_array($value) || is_array($places)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $places); + } + try { - $value = self::validateValue(Functions::flattenSingleValue($value)); + $value = self::validateValue($value); $value = self::validateOctal($value); - $places = self::validatePlaces(Functions::flattenSingleValue($places)); + $places = self::validatePlaces($places); } catch (Exception $e) { return $e->getMessage(); } $hexVal = strtoupper(dechex((int) self::toDecimal($value))); - $hexVal = (PHP_INT_SIZE === 4 && strlen($value) === 10 && $value[0] >= '4') ? "FF$hexVal" : $hexVal; + $hexVal = (PHP_INT_SIZE === 4 && strlen($value) === 10 && $value[0] >= '4') ? "FF{$hexVal}" : $hexVal; return self::nbrConversionFormat($hexVal, $places); } @@ -137,7 +166,7 @@ class ConvertOctal extends ConvertBase { $numDigits = (int) preg_match_all('/[01234567]/', $value); if (strlen($value) > $numDigits || $numDigits > 10) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php index d169ae54bb0..b7c298dbc39 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ConvertUOM.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ConvertUOM { + use ArrayEnabled; + public const CATEGORY_WEIGHT_AND_MASS = 'Weight and Mass'; public const CATEGORY_DISTANCE = 'Distance'; public const CATEGORY_TIME = 'Time'; @@ -103,6 +106,7 @@ class ConvertUOM 'W' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'w' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Watt', 'AllowPrefix' => true], 'PS' => ['Group' => self::CATEGORY_POWER, 'Unit Name' => 'Pferdestärke', 'AllowPrefix' => false], + // Magnetism 'T' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Tesla', 'AllowPrefix' => true], 'ga' => ['Group' => self::CATEGORY_MAGNETISM, 'Unit Name' => 'Gauss', 'AllowPrefix' => true], // Temperature @@ -518,33 +522,39 @@ class ConvertUOM * Excel Function: * CONVERT(value,fromUOM,toUOM) * - * @param float|int $value the value in fromUOM to convert - * @param string $fromUOM the units for value - * @param string $toUOM the units for the result + * @param array|float|int|string $value the value in fromUOM to convert + * Or can be an array of values + * @param array|string $fromUOM the units for value + * Or can be an array of values + * @param array|string $toUOM the units for the result + * Or can be an array of values * - * @return float|string + * @return array|float|string Result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function CONVERT($value, $fromUOM, $toUOM) { - $value = Functions::flattenSingleValue($value); - $fromUOM = Functions::flattenSingleValue($fromUOM); - $toUOM = Functions::flattenSingleValue($toUOM); + if (is_array($value) || is_array($fromUOM) || is_array($toUOM)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $fromUOM, $toUOM); + } if (!is_numeric($value)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } try { [$fromUOM, $fromCategory, $fromMultiplier] = self::getUOMDetails($fromUOM); [$toUOM, $toCategory, $toMultiplier] = self::getUOMDetails($toUOM); } catch (Exception $e) { - return Functions::NA(); + return ExcelError::NA(); } if ($fromCategory !== $toCategory) { - return Functions::NA(); + return ExcelError::NA(); } + // @var float $value $value *= $fromMultiplier; if (($fromUOM === $toUOM) && ($fromMultiplier === $toMultiplier)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php index 01630af3412..c0202ea20b3 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/EngineeringValidations.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class EngineeringValidations { @@ -13,7 +13,7 @@ class EngineeringValidations public static function validateFloat($value): float { if (!is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (float) $value; @@ -25,7 +25,7 @@ class EngineeringValidations public static function validateInt($value): int { if (!is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (int) floor((float) $value); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Erf.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Erf.php index db87ec0d386..6254d4776c5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Erf.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/Erf.php @@ -2,10 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Erf { + use ArrayEnabled; + private static $twoSqrtPi = 1.128379167095512574; /** @@ -22,15 +26,20 @@ class Erf * ERF(lower[,upper]) * * @param mixed $lower Lower bound float for integrating ERF + * Or can be an array of values * @param mixed $upper Upper bound float for integrating ERF. * If omitted, ERF integrates between zero and lower_limit + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function ERF($lower, $upper = null) { - $lower = Functions::flattenSingleValue($lower); - $upper = Functions::flattenSingleValue($upper); + if (is_array($lower) || is_array($upper)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $lower, $upper); + } if (is_numeric($lower)) { if ($upper === null) { @@ -41,7 +50,7 @@ class Erf } } - return Functions::VALUE(); + return ExcelError::VALUE(); } /** @@ -53,12 +62,17 @@ class Erf * ERF.PRECISE(limit) * * @param mixed $limit Float bound for integrating ERF, other bound is zero + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function ERFPRECISE($limit) { - $limit = Functions::flattenSingleValue($limit); + if (is_array($limit)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $limit); + } return self::ERF($limit); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php index c57a28f490c..7b023bee593 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php @@ -2,10 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ErfC { + use ArrayEnabled; + /** * ERFC. * @@ -20,18 +24,23 @@ class ErfC * ERFC(x) * * @param mixed $value The float lower bound for integrating ERFC + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function ERFC($value) { - $value = Functions::flattenSingleValue($value); + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } if (is_numeric($value)) { return self::erfcValue($value); } - return Functions::VALUE(); + return ExcelError::VALUE(); } // @@ -45,7 +54,7 @@ class ErfC return 1 - Erf::erfValue($value); } if ($value < 0) { - return 2 - self::ERFC(-$value); + return 2 - self::erfcValue(-$value); } $a = $n = 1; $b = $c = $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ExceptionHandler.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ExceptionHandler.php index 41e51d4aea6..6461961a07e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ExceptionHandler.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/ExceptionHandler.php @@ -9,7 +9,9 @@ class ExceptionHandler */ public function __construct() { - set_error_handler([Exception::class, 'errorHandlerCallback'], E_ALL); + /** @var callable */ + $callable = [Exception::class, 'errorHandlerCallback']; + set_error_handler($callable, E_ALL); } /** diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial.php index 9d933b4a921..4215a5163d1 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial.php @@ -587,10 +587,10 @@ class Financial * @see Financial\Dollar::decimal() * Use the decimal() method in the Financial\Dollar class instead * - * @param float $fractional_dollar Fractional Dollar - * @param int $fraction Fraction + * @param array|float $fractional_dollar Fractional Dollar + * @param array|int $fraction Fraction * - * @return float|string + * @return array|float|string */ public static function DOLLARDE($fractional_dollar = null, $fraction = 0) { @@ -612,10 +612,10 @@ class Financial * @see Financial\Dollar::fractional() * Use the fractional() method in the Financial\Dollar class instead * - * @param float $decimal_dollar Decimal Dollar - * @param int $fraction Fraction + * @param array|float $decimal_dollar Decimal Dollar + * @param array|int $fraction Fraction * - * @return float|string + * @return array|float|string */ public static function DOLLARFR($decimal_dollar = null, $fraction = 0) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Amortization.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Amortization.php index ba7fb5210d7..691ba40ce1f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Amortization.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Amortization.php @@ -70,10 +70,12 @@ class Amortization return $e->getMessage(); } - $yearFrac = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); - if (is_string($yearFrac)) { - return $yearFrac; + $yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); + if (is_string($yearFracx)) { + return $yearFracx; } + /** @var float */ + $yearFrac = $yearFracx; $amortiseCoeff = self::getAmortizationCoefficient($rate); @@ -161,14 +163,16 @@ class Amortization $fCostDelta = $cost - $salvage; // Note, quirky variation for leap years on the YEARFRAC for this function $purchasedYear = DateTimeExcel\DateParts::year($purchased); - $yearFrac = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); - if (is_string($yearFrac)) { - return $yearFrac; + $yearFracx = DateTimeExcel\YearFrac::fraction($purchased, $firstPeriod, $basis); + if (is_string($yearFracx)) { + return $yearFracx; } + /** @var float */ + $yearFrac = $yearFracx; if ( ($basis == FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL) && - ($yearFrac < 1) && (DateTimeExcel\Helpers::isLeapYear($purchasedYear)) + ($yearFrac < 1) && (Functions::scalar(DateTimeExcel\Helpers::isLeapYear($purchasedYear))) ) { $yearFrac *= 365 / 366; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/CashFlowValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/CashFlowValidations.php index e4c8a3a4ceb..8ebe9ed729d 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/CashFlowValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/CashFlowValidations.php @@ -5,7 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Financial\FinancialValidations; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class CashFlowValidations extends FinancialValidations { @@ -29,7 +29,7 @@ class CashFlowValidations extends FinancialValidations $type !== FinancialConstants::PAYMENT_END_OF_PERIOD && $type !== FinancialConstants::PAYMENT_BEGINNING_OF_PERIOD ) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $rate; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php index e8a159ce389..a19c37f60f0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Periodic { @@ -94,7 +95,7 @@ class Periodic // Validate parameters if ($numberOfPeriods < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::calculatePresentValue($rate, $numberOfPeriods, $payment, $futureValue, $type); @@ -138,7 +139,7 @@ class Periodic // Validate parameters if ($payment == 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::calculatePeriods($rate, $payment, $presentValue, $futureValue, $type); @@ -187,7 +188,7 @@ class Periodic ) { if ($rate != 0.0) { if ($presentValue == 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return log(($payment * (1 + $rate * $type) / $rate - $futureValue) / diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php index b7f6011c315..b7aaffd82cc 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Cumulative { @@ -57,7 +58,7 @@ class Cumulative // Validate parameters if ($start < 1 || $start > $end) { - return Functions::NAN(); + return ExcelError::NAN(); } // Calculate @@ -122,7 +123,7 @@ class Cumulative // Validate parameters if ($start < 1 || $start > $end) { - return Functions::VALUE(); + return ExcelError::VALUE(); } // Calculate diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php index 56d2e379298..804120823bf 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Interest { @@ -59,7 +60,7 @@ class Interest // Validate parameters if ($period <= 0 || $period > $numberOfPeriods) { - return Functions::NAN(); + return ExcelError::NAN(); } // Calculate @@ -106,7 +107,7 @@ class Interest // Validate parameters if ($period <= 0 || $period > $numberOfPeriods) { - return Functions::NAN(); + return ExcelError::NAN(); } // Return value @@ -193,13 +194,13 @@ class Interest $rate = $rate1; } - return $close ? $rate : Functions::NAN(); + return $close ? $rate : ExcelError::NAN(); } private static function rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type) { if ($rate == 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } $tt1 = ($rate + 1) ** $numberOfPeriods; $tt2 = ($rate + 1) ** ($numberOfPeriods - 1); @@ -208,7 +209,7 @@ class Interest * ($rate * $type + 1) / ($rate * $rate) + $numberOfPeriods * $payment * $tt2 * ($rate * $type + 1) / $rate + $payment * ($tt1 - 1) * $type / $rate; if ($denominator == 0) { - return Functions::NAN(); + return ExcelError::NAN(); } return $numerator / $denominator; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php index e103f923666..83965f9d3bd 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\CashFlowValidations; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Payments { @@ -97,7 +98,7 @@ class Payments // Validate parameters if ($period <= 0 || $period > $numberOfPeriods) { - return Functions::NAN(); + return ExcelError::NAN(); } // Calculate diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php index a30634da3be..058e89c64ef 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Single { @@ -67,7 +68,7 @@ class Single // Validate parameters if ($rate <= 0.0 || $presentValue <= 0.0 || $futureValue <= 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return (log($futureValue) - log($presentValue)) / log(1 + $rate); @@ -100,7 +101,7 @@ class Single // Validate parameters if ($periods <= 0.0 || $presentValue <= 0.0 || $futureValue < 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return ($futureValue / $presentValue) ** (1 / $periods) - 1; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php index 8986146c11c..b81261351e6 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class NonPeriodic { @@ -12,6 +13,8 @@ class NonPeriodic const FINANCIAL_PRECISION = 1.0e-08; + const DEFAULT_GUESS = 0.1; + /** * XIRR. * @@ -25,11 +28,11 @@ class NonPeriodic * @param mixed[] $dates A series of payment dates * The first payment date indicates the beginning of the schedule of payments * All other dates must be later than this date, but they may occur in any order - * @param float $guess An optional guess at the expected answer + * @param mixed $guess An optional guess at the expected answer * * @return float|string */ - public static function rate($values, $dates, $guess = 0.1) + public static function rate($values, $dates, $guess = self::DEFAULT_GUESS) { $rslt = self::xirrPart1($values, $dates); if ($rslt !== '') { @@ -37,9 +40,13 @@ class NonPeriodic } // create an initial range, with a root somewhere between 0 and guess - $guess = Functions::flattenSingleValue($guess); + $guess = Functions::flattenSingleValue($guess) ?? self::DEFAULT_GUESS; + if (!is_numeric($guess)) { + return ExcelError::VALUE(); + } + $guess = ($guess + 0.0) ?: self::DEFAULT_GUESS; $x1 = 0.0; - $x2 = $guess ?: 0.1; + $x2 = $guess + 0.0; $f1 = self::xnpvOrdered($x1, $values, $dates, false); $f2 = self::xnpvOrdered($x2, $values, $dates, false); $found = false; @@ -54,13 +61,15 @@ class NonPeriodic break; } elseif (abs($f1) < abs($f2)) { - $f1 = self::xnpvOrdered($x1 += 1.6 * ($x1 - $x2), $values, $dates, false); + $x1 += 1.6 * ($x1 - $x2); + $f1 = self::xnpvOrdered($x1, $values, $dates, false); } else { - $f2 = self::xnpvOrdered($x2 += 1.6 * ($x2 - $x1), $values, $dates, false); + $x2 += 1.6 * ($x2 - $x1); + $f2 = self::xnpvOrdered($x2, $values, $dates, false); } } if (!$found) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::xirrPart3($values, $dates, $x1, $x2); @@ -104,13 +113,15 @@ class NonPeriodic */ private static function xirrPart1(&$values, &$dates): string { - if (!is_array($values) && !is_array($dates)) { - return Functions::NA(); - } $values = Functions::flattenArray($values); $dates = Functions::flattenArray($dates); + $valuesIsArray = count($values) > 1; + $datesIsArray = count($dates) > 1; + if (!$valuesIsArray && !$datesIsArray) { + return ExcelError::NA(); + } if (count($values) != count($dates)) { - return Functions::NAN(); + return ExcelError::NAN(); } $datesCount = count($dates); @@ -133,7 +144,7 @@ class NonPeriodic for ($i = 0; $i < $valCount; ++$i) { $fld = $values[$i]; if (!is_numeric($fld)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } elseif ($fld > 0) { $foundpos = true; } elseif ($fld < 0) { @@ -141,7 +152,7 @@ class NonPeriodic } } if (!self::bothNegAndPos($foundneg, $foundpos)) { - return Functions::NAN(); + return ExcelError::NAN(); } return ''; @@ -161,7 +172,7 @@ class NonPeriodic $dx = $x1 - $x2; } - $rslt = Functions::VALUE(); + $rslt = ExcelError::VALUE(); for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { $dx *= 0.5; $x_mid = $rtb + $dx; @@ -203,7 +214,7 @@ class NonPeriodic $xnpv = 0.0; for ($i = 0; $i < $valCount; ++$i) { if (!is_numeric($values[$i])) { - return Functions::VALUE(); + return ExcelError::VALUE(); } try { @@ -212,17 +223,21 @@ class NonPeriodic return $e->getMessage(); } if ($date0 > $datei) { - $dif = $ordered ? Functions::NAN() : -((int) DateTimeExcel\Difference::interval($datei, $date0, 'd')); + $dif = $ordered ? ExcelError::NAN() : -((int) DateTimeExcel\Difference::interval($datei, $date0, 'd')); } else { $dif = DateTimeExcel\Difference::interval($date0, $datei, 'd'); } if (!is_numeric($dif)) { return $dif; } - $xnpv += $values[$i] / (1 + $rate) ** ($dif / 365); + if ($rate <= -1.0) { + $xnpv += -abs($values[$i]) / (-1 - $rate) ** ($dif / 365); + } else { + $xnpv += $values[$i] / (1 + $rate) ** ($dif / 365); + } } - return is_finite($xnpv) ? $xnpv : Functions::VALUE(); + return is_finite($xnpv) ? $xnpv : ExcelError::VALUE(); } /** @@ -231,14 +246,14 @@ class NonPeriodic private static function validateXnpv($rate, array $values, array $dates): void { if (!is_numeric($rate)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } $valCount = count($values); if ($valCount != count($dates)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php index c42df0c39eb..49d063c9791 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow\Variable; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Periodic { @@ -33,7 +34,7 @@ class Periodic public static function rate($values, $guess = 0.1) { if (!is_array($values)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $values = Functions::flattenArray($values); $guess = Functions::flattenSingleValue($guess); @@ -54,7 +55,7 @@ class Periodic } } if (($f1 * $f2) > 0.0) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $f = self::presentValue($x1, $values); @@ -78,7 +79,7 @@ class Periodic } } - return Functions::VALUE(); + return ExcelError::VALUE(); } /** @@ -101,7 +102,7 @@ class Periodic public static function modifiedRate($values, $financeRate, $reinvestmentRate) { if (!is_array($values)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $values = Functions::flattenArray($values); $financeRate = Functions::flattenSingleValue($financeRate); @@ -121,13 +122,13 @@ class Periodic } if (($npvNeg === 0.0) || ($npvPos === 0.0) || ($reinvestmentRate <= -1.0)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $mirr = ((-$npvPos * $rr ** $n) / ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0; - return is_finite($mirr) ? $mirr : Functions::VALUE(); + return is_finite($mirr) ? $mirr : ExcelError::VALUE(); } /** diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index 3fd6c1d2de6..6138075c573 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -7,6 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Date; class Coupons @@ -64,9 +65,9 @@ class Coupons return $e->getMessage(); } - $daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis); + $daysPerYear = Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); if (is_string($daysPerYear)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); @@ -134,7 +135,7 @@ class Coupons case FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL: // Actual/actual if ($frequency == FinancialConstants::FREQUENCY_ANNUAL) { - $daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis); + $daysPerYear = (int) Helpers::daysPerYear(Functions::scalar(DateTimeExcel\DateParts::year($settlement)), $basis); return $daysPerYear / $frequency; } @@ -198,6 +199,7 @@ class Coupons return $e->getMessage(); } + /** @var int */ $daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis); $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); @@ -409,7 +411,7 @@ class Coupons private static function validateCouponPeriod(float $settlement, float $maturity): void { if ($settlement >= $maturity) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php index 650a4861b29..3d72757a823 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Depreciation { @@ -117,7 +118,7 @@ class Depreciation } if ($period > $life) { - return Functions::NAN(); + return ExcelError::NAN(); } // Loop through each period calculating the depreciation @@ -161,7 +162,7 @@ class Depreciation } if ($life === 0.0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } return ($cost - $salvage) / $life; @@ -196,7 +197,7 @@ class Depreciation } if ($period > $life) { - return Functions::NAN(); + return ExcelError::NAN(); } $syd = (($cost - $salvage) * ($life - $period + 1) * 2) / ($life * ($life + 1)); @@ -208,7 +209,7 @@ class Depreciation { $cost = FinancialValidations::validateFloat($cost); if ($cost < 0.0 && $negativeValueAllowed === false) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $cost; @@ -218,7 +219,7 @@ class Depreciation { $salvage = FinancialValidations::validateFloat($salvage); if ($salvage < 0.0 && $negativeValueAllowed === false) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $salvage; @@ -228,7 +229,7 @@ class Depreciation { $life = FinancialValidations::validateFloat($life); if ($life < 0.0 && $negativeValueAllowed === false) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $life; @@ -238,7 +239,7 @@ class Depreciation { $period = FinancialValidations::validateFloat($period); if ($period <= 0.0 && $negativeValueAllowed === false) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $period; @@ -248,7 +249,7 @@ class Depreciation { $month = FinancialValidations::validateInt($month); if ($month < 1) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $month; @@ -258,7 +259,7 @@ class Depreciation { $factor = FinancialValidations::validateFloat($factor); if ($factor <= 0.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $factor; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Dollar.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Dollar.php index 7bebb39178f..b1f0d25c7b4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Dollar.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Dollar.php @@ -2,23 +2,34 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\TextData\Format; class Dollar { + use ArrayEnabled; + /** * DOLLAR. * * This function converts a number to text using currency format, with the decimals rounded to the specified place. * The format used is $#,##0.00_);($#,##0.00).. * - * @param mixed $number The value to format + * @param mixed $number The value to format, or can be an array of numbers + * Or can be an array of values * @param mixed $precision The number of digits to display to the right of the decimal point (as an integer). * If precision is negative, number is rounded to the left of the decimal point. * If you omit precision, it is assumed to be 2 + * Or can be an array of precision values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions */ - public static function format($number, $precision = 2): string + public static function format($number, $precision = 2) { return Format::DOLLAR($number, $precision); } @@ -34,25 +45,37 @@ class Dollar * DOLLARDE(fractional_dollar,fraction) * * @param mixed $fractionalDollar Fractional Dollar + * Or can be an array of values * @param mixed $fraction Fraction + * Or can be an array of values * - * @return float|string + * @return array|float|string */ public static function decimal($fractionalDollar = null, $fraction = 0) { - $fractionalDollar = Functions::flattenSingleValue($fractionalDollar); - $fraction = (int) Functions::flattenSingleValue($fraction); + if (is_array($fractionalDollar) || is_array($fraction)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $fractionalDollar, $fraction); + } - // Validate parameters - if ($fractionalDollar === null || $fraction < 0) { - return Functions::NAN(); + try { + $fractionalDollar = FinancialValidations::validateFloat( + Functions::flattenSingleValue($fractionalDollar) ?? 0.0 + ); + $fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction)); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Additional parameter validations + if ($fraction < 0) { + return ExcelError::NAN(); } if ($fraction == 0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } - $dollars = floor($fractionalDollar); - $cents = fmod($fractionalDollar, 1); + $dollars = ($fractionalDollar < 0) ? ceil($fractionalDollar) : floor($fractionalDollar); + $cents = fmod($fractionalDollar, 1.0); $cents /= $fraction; $cents *= 10 ** ceil(log10($fraction)); @@ -70,24 +93,36 @@ class Dollar * DOLLARFR(decimal_dollar,fraction) * * @param mixed $decimalDollar Decimal Dollar + * Or can be an array of values * @param mixed $fraction Fraction + * Or can be an array of values * - * @return float|string + * @return array|float|string */ public static function fractional($decimalDollar = null, $fraction = 0) { - $decimalDollar = Functions::flattenSingleValue($decimalDollar); - $fraction = (int) Functions::flattenSingleValue($fraction); + if (is_array($decimalDollar) || is_array($fraction)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $decimalDollar, $fraction); + } - // Validate parameters - if ($decimalDollar === null || $fraction < 0) { - return Functions::NAN(); + try { + $decimalDollar = FinancialValidations::validateFloat( + Functions::flattenSingleValue($decimalDollar) ?? 0.0 + ); + $fraction = FinancialValidations::validateInt(Functions::flattenSingleValue($fraction)); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Additional parameter validations + if ($fraction < 0) { + return ExcelError::NAN(); } if ($fraction == 0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } - $dollars = floor($decimalDollar); + $dollars = ($decimalDollar < 0.0) ? ceil($decimalDollar) : floor($decimalDollar); $cents = fmod($decimalDollar, 1); $cents *= $fraction; $cents *= 10 ** (-ceil(log10($fraction))); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php index 310e0051c90..1b04419748c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/FinancialValidations.php @@ -5,7 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class FinancialValidations { @@ -39,7 +39,7 @@ class FinancialValidations public static function validateFloat($value): float { if (!is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (float) $value; @@ -51,7 +51,7 @@ class FinancialValidations public static function validateInt($value): int { if (!is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (int) floor((float) $value); @@ -64,7 +64,7 @@ class FinancialValidations { $rate = self::validateFloat($rate); if ($rate < 0.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $rate; @@ -81,7 +81,7 @@ class FinancialValidations ($frequency !== FinancialConstants::FREQUENCY_SEMI_ANNUAL) && ($frequency !== FinancialConstants::FREQUENCY_QUARTERLY) ) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $frequency; @@ -93,12 +93,12 @@ class FinancialValidations public static function validateBasis($basis): int { if (!is_numeric($basis)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } $basis = (int) $basis; if (($basis < 0) || ($basis > 4)) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $basis; @@ -111,7 +111,7 @@ class FinancialValidations { $price = self::validateFloat($price); if ($price < 0.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $price; @@ -124,7 +124,7 @@ class FinancialValidations { $parValue = self::validateFloat($parValue); if ($parValue < 0.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $parValue; @@ -137,7 +137,7 @@ class FinancialValidations { $yield = self::validateFloat($yield); if ($yield < 0.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $yield; @@ -150,7 +150,7 @@ class FinancialValidations { $discount = self::validateFloat($discount); if ($discount <= 0.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $discount; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Helpers.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Helpers.php index d339b13416c..c7f1f46a271 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Helpers.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Helpers.php @@ -5,7 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Helpers { @@ -27,7 +27,7 @@ class Helpers public static function daysPerYear($year, $basis = 0) { if (!is_numeric($basis)) { - return Functions::NAN(); + return ExcelError::NAN(); } switch ($basis) { @@ -41,7 +41,7 @@ class Helpers return (DateTimeExcel\Helpers::isLeapYear($year)) ? 366 : 365; } - return Functions::NAN(); + return ExcelError::NAN(); } /** diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php index 72df31e1641..1cbe2657b54 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class InterestRate { @@ -34,7 +35,7 @@ class InterestRate } if ($nominalRate <= 0 || $periodsPerYear < 1) { - return Functions::NAN(); + return ExcelError::NAN(); } return ((1 + $nominalRate / $periodsPerYear) ** $periodsPerYear) - 1; @@ -63,7 +64,7 @@ class InterestRate } if ($effectiveRate <= 0 || $periodsPerYear < 1) { - return Functions::NAN(); + return ExcelError::NAN(); } // Calculate diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php index e167429b72b..95996b4e956 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php @@ -78,12 +78,12 @@ class AccruedInterest return $e->getMessage(); } - $daysBetweenIssueAndSettlement = YearFrac::fraction($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis)); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; } - $daysBetweenFirstInterestAndSettlement = YearFrac::fraction($firstInterest, $settlement, $basis); + $daysBetweenFirstInterestAndSettlement = Functions::scalar(YearFrac::fraction($firstInterest, $settlement, $basis)); if (!is_numeric($daysBetweenFirstInterestAndSettlement)) { // return date error return $daysBetweenFirstInterestAndSettlement; @@ -140,7 +140,7 @@ class AccruedInterest return $e->getMessage(); } - $daysBetweenIssueAndSettlement = YearFrac::fraction($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = Functions::scalar(YearFrac::fraction($issue, $settlement, $basis)); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php index 7d8d5a3210e..de1748a19d1 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstan use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Price { @@ -134,7 +135,7 @@ class Price return $e->getMessage(); } - $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -194,23 +195,23 @@ class Price return $e->getMessage(); } - $daysPerYear = Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis); + $daysPerYear = Functions::scalar(Helpers::daysPerYear(DateTimeExcel\DateParts::year($settlement), $basis)); if (!is_numeric($daysPerYear)) { return $daysPerYear; } - $daysBetweenIssueAndSettlement = DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis)); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; } $daysBetweenIssueAndSettlement *= $daysPerYear; - $daysBetweenIssueAndMaturity = DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis); + $daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis)); if (!is_numeric($daysBetweenIssueAndMaturity)) { // return date error return $daysBetweenIssueAndMaturity; } $daysBetweenIssueAndMaturity *= $daysPerYear; - $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -270,7 +271,7 @@ class Price } if ($investment <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Rates.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Rates.php index c5c5211b70e..f8d8673672e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Rates.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Rates.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Rates { @@ -60,10 +61,10 @@ class Rates } if ($price <= 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } - $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -123,10 +124,10 @@ class Rates } if ($investment <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } - $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/SecurityValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/SecurityValidations.php index 497197b843d..d3196f0f79f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/SecurityValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/SecurityValidations.php @@ -4,7 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\FinancialValidations; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class SecurityValidations extends FinancialValidations { @@ -23,7 +23,7 @@ class SecurityValidations extends FinancialValidations public static function validateSecurityPeriod($settlement, $maturity): void { if ($settlement >= $maturity) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } } @@ -34,7 +34,7 @@ class SecurityValidations extends FinancialValidations { $redemption = self::validateFloat($redemption); if ($redemption <= 0.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $redemption; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php index aa6269354bb..bb2e8ae2255 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php @@ -61,7 +61,7 @@ class Yields if (!is_numeric($daysPerYear)) { return $daysPerYear; } - $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -126,19 +126,19 @@ class Yields if (!is_numeric($daysPerYear)) { return $daysPerYear; } - $daysBetweenIssueAndSettlement = DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $settlement, $basis)); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; } $daysBetweenIssueAndSettlement *= $daysPerYear; - $daysBetweenIssueAndMaturity = DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis); + $daysBetweenIssueAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($issue, $maturity, $basis)); if (!is_numeric($daysBetweenIssueAndMaturity)) { // return date error return $daysBetweenIssueAndMaturity; } $daysBetweenIssueAndMaturity *= $daysPerYear; - $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = Functions::scalar(DateTimeExcel\YearFrac::fraction($settlement, $maturity, $basis)); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php index c60af0b0d0c..7ee34f73240 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Constants as FinancialConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class TreasuryBill { @@ -38,17 +39,17 @@ class TreasuryBill } if ($discount <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $daysBetweenSettlementAndMaturity = $maturity - $settlement; $daysPerYear = Helpers::daysPerYear( - DateTimeExcel\DateParts::year($maturity), + Functions::scalar(DateTimeExcel\DateParts::year($maturity)), FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL ); if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } return (365 * $discount) / (360 - $discount * $daysBetweenSettlementAndMaturity); @@ -83,22 +84,22 @@ class TreasuryBill } if ($discount <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $daysBetweenSettlementAndMaturity = $maturity - $settlement; $daysPerYear = Helpers::daysPerYear( - DateTimeExcel\DateParts::year($maturity), + Functions::scalar(DateTimeExcel\DateParts::year($maturity)), FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL ); if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $price = 100 * (1 - (($discount * $daysBetweenSettlementAndMaturity) / 360)); if ($price < 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return $price; @@ -134,12 +135,12 @@ class TreasuryBill $daysBetweenSettlementAndMaturity = $maturity - $settlement; $daysPerYear = Helpers::daysPerYear( - DateTimeExcel\DateParts::year($maturity), + Functions::scalar(DateTimeExcel\DateParts::year($maturity)), FinancialConstants::BASIS_DAYS_PER_YEAR_ACTUAL ); if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } return ((100 - $price) / $price) * (360 / $daysBetweenSettlementAndMaturity); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php index ddf45b23be8..f71d96fc76d 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/FormulaParser.php @@ -61,7 +61,7 @@ class FormulaParser /** * Create a new FormulaParser. * - * @param string $formula Formula to parse + * @param ?string $formula Formula to parse */ public function __construct($formula = '') { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php index 75d4582b2c7..172f2022229 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Functions.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Shared\Date; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Functions { @@ -41,30 +40,14 @@ class Functions */ protected static $returnDateType = self::RETURNDATE_EXCEL; - /** - * List of error codes. - * - * @var array - */ - protected static $errorCodes = [ - 'null' => '#NULL!', - 'divisionbyzero' => '#DIV/0!', - 'value' => '#VALUE!', - 'reference' => '#REF!', - 'name' => '#NAME?', - 'num' => '#NUM!', - 'na' => '#N/A', - 'gettingdata' => '#GETTING_DATA', - ]; - /** * Set the Compatibility Mode. * * @param string $compatibilityMode Compatibility Mode - * Permitted values are: - * Functions::COMPATIBILITY_EXCEL 'Excel' - * Functions::COMPATIBILITY_GNUMERIC 'Gnumeric' - * Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc' + * Permitted values are: + * Functions::COMPATIBILITY_EXCEL 'Excel' + * Functions::COMPATIBILITY_GNUMERIC 'Gnumeric' + * Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc' * * @return bool (Success or Failure) */ @@ -87,10 +70,10 @@ class Functions * Return the current Compatibility Mode. * * @return string Compatibility Mode - * Possible Return values are: - * Functions::COMPATIBILITY_EXCEL 'Excel' - * Functions::COMPATIBILITY_GNUMERIC 'Gnumeric' - * Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc' + * Possible Return values are: + * Functions::COMPATIBILITY_EXCEL 'Excel' + * Functions::COMPATIBILITY_GNUMERIC 'Gnumeric' + * Functions::COMPATIBILITY_OPENOFFICE 'OpenOfficeCalc' */ public static function getCompatibilityMode() { @@ -98,13 +81,13 @@ class Functions } /** - * Set the Return Date Format used by functions that return a date/time (Excel, PHP Serialized Numeric or PHP Object). + * Set the Return Date Format used by functions that return a date/time (Excel, PHP Serialized Numeric or PHP DateTime Object). * * @param string $returnDateType Return Date Format - * Permitted values are: - * Functions::RETURNDATE_UNIX_TIMESTAMP 'P' - * Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O' - * Functions::RETURNDATE_EXCEL 'E' + * Permitted values are: + * Functions::RETURNDATE_UNIX_TIMESTAMP 'P' + * Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O' + * Functions::RETURNDATE_EXCEL 'E' * * @return bool Success or failure */ @@ -127,10 +110,10 @@ class Functions * Return the current Return Date Format for functions that return a date/time (Excel, PHP Serialized Numeric or PHP Object). * * @return string Return Date Format - * Possible Return values are: - * Functions::RETURNDATE_UNIX_TIMESTAMP 'P' - * Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O' - * Functions::RETURNDATE_EXCEL 'E' + * Possible Return values are: + * Functions::RETURNDATE_UNIX_TIMESTAMP 'P' + * Functions::RETURNDATE_PHP_DATETIME_OBJECT 'O' + * Functions::RETURNDATE_EXCEL ' 'E' */ public static function getReturnDateType() { @@ -147,92 +130,6 @@ class Functions return '#Not Yet Implemented'; } - /** - * DIV0. - * - * @return string #Not Yet Implemented - */ - public static function DIV0() - { - return self::$errorCodes['divisionbyzero']; - } - - /** - * NA. - * - * Excel Function: - * =NA() - * - * Returns the error value #N/A - * #N/A is the error value that means "no value is available." - * - * @return string #N/A! - */ - public static function NA() - { - return self::$errorCodes['na']; - } - - /** - * NaN. - * - * Returns the error value #NUM! - * - * @return string #NUM! - */ - public static function NAN() - { - return self::$errorCodes['num']; - } - - /** - * NAME. - * - * Returns the error value #NAME? - * - * @return string #NAME? - */ - public static function NAME() - { - return self::$errorCodes['name']; - } - - /** - * REF. - * - * Returns the error value #REF! - * - * @return string #REF! - */ - public static function REF() - { - return self::$errorCodes['reference']; - } - - /** - * NULL. - * - * Returns the error value #NULL! - * - * @return string #NULL! - */ - public static function null() - { - return self::$errorCodes['null']; - } - - /** - * VALUE. - * - * Returns the error value #VALUE! - * - * @return string #VALUE! - */ - public static function VALUE() - { - return self::$errorCodes['value']; - } - public static function isMatrixValue($idx) { return (substr_count($idx, '.') <= 1) || (preg_match('/\.[A-Z]/', $idx) > 0); @@ -240,7 +137,7 @@ class Functions public static function isValue($idx) { - return substr_count($idx, '.') == 0; + return substr_count($idx, '.') === 0; } public static function isCellValue($idx) @@ -255,11 +152,15 @@ class Functions if ($condition === '') { return '=""'; } - if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='])) { + if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='], true)) { $condition = self::operandSpecialHandling($condition); if (is_bool($condition)) { return '=' . ($condition ? 'TRUE' : 'FALSE'); } elseif (!is_numeric($condition)) { + if ($condition !== '""') { // Not an empty string + // Escape any quotes in the string value + $condition = (string) preg_replace('/"/ui', '""', $condition); + } $condition = Calculation::wrapResult(strtoupper($condition)); } @@ -300,26 +201,142 @@ class Functions return $operand; } + /** + * NULL. + * + * Returns the error value #NULL! + * + * @Deprecated 1.23.0 + * + * @return string #NULL! + * + *@see Information\ExcelError::null() + * Use the null() method in the Information\Error class instead + */ + public static function null() + { + return Information\ExcelError::null(); + } + + /** + * NaN. + * + * Returns the error value #NUM! + * + * @Deprecated 1.23.0 + * + * @return string #NUM! + * + * @see Information\ExcelError::NAN() + * Use the NAN() method in the Information\Error class instead + */ + public static function NAN() + { + return Information\ExcelError::NAN(); + } + + /** + * REF. + * + * Returns the error value #REF! + * + * @Deprecated 1.23.0 + * + * @return string #REF! + * + * @see Information\ExcelError::REF() + * Use the REF() method in the Information\Error class instead + */ + public static function REF() + { + return Information\ExcelError::REF(); + } + + /** + * NA. + * + * Excel Function: + * =NA() + * + * Returns the error value #N/A + * #N/A is the error value that means "no value is available." + * + * @Deprecated 1.23.0 + * + * @return string #N/A! + * + * @see Information\ExcelError::NA() + * Use the NA() method in the Information\Error class instead + */ + public static function NA() + { + return Information\ExcelError::NA(); + } + + /** + * VALUE. + * + * Returns the error value #VALUE! + * + * @Deprecated 1.23.0 + * + * @return string #VALUE! + * + * @see Information\ExcelError::VALUE() + * Use the VALUE() method in the Information\Error class instead + */ + public static function VALUE() + { + return Information\ExcelError::VALUE(); + } + + /** + * NAME. + * + * Returns the error value #NAME? + * + * @Deprecated 1.23.0 + * + * @return string #NAME? + * + * @see Information\ExcelError::NAME() + * Use the NAME() method in the Information\Error class instead + */ + public static function NAME() + { + return Information\ExcelError::NAME(); + } + + /** + * DIV0. + * + * @Deprecated 1.23.0 + * + * @return string #Not Yet Implemented + * + *@see Information\ExcelError::DIV0() + * Use the DIV0() method in the Information\Error class instead + */ + public static function DIV0() + { + return Information\ExcelError::DIV0(); + } + /** * ERROR_TYPE. * * @param mixed $value Value to check * - * @return int|string + * @Deprecated 1.23.0 + * + * @return array|int|string + * + * @see Information\ExcelError::type() + * Use the type() method in the Information\Error class instead */ public static function errorType($value = '') { - $value = self::flattenSingleValue($value); - - $i = 1; - foreach (self::$errorCodes as $errorCode) { - if ($value === $errorCode) { - return $i; - } - ++$i; - } - - return self::NA(); + return Information\ExcelError::type($value); } /** @@ -327,15 +344,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isBlank() + * Use the isBlank() method in the Information\Value class instead + * + * @return array|bool */ public static function isBlank($value = null) { - if ($value !== null) { - $value = self::flattenSingleValue($value); - } - - return $value === null; + return Information\Value::isBlank($value); } /** @@ -343,13 +361,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isErr() + * Use the isErr() method in the Information\Value class instead + * + * @return array|bool */ public static function isErr($value = '') { - $value = self::flattenSingleValue($value); - - return self::isError($value) && (!self::isNa(($value))); + return Information\ErrorValue::isErr($value); } /** @@ -357,17 +378,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isError() + * Use the isError() method in the Information\Value class instead + * + * @return array|bool */ public static function isError($value = '') { - $value = self::flattenSingleValue($value); - - if (!is_string($value)) { - return false; - } - - return in_array($value, self::$errorCodes); + return Information\ErrorValue::isError($value); } /** @@ -375,13 +395,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isNa() + * Use the isNa() method in the Information\Value class instead + * + * @return array|bool */ public static function isNa($value = '') { - $value = self::flattenSingleValue($value); - - return $value === self::NA(); + return Information\ErrorValue::isNa($value); } /** @@ -389,19 +412,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool|string + * @Deprecated 1.23.0 + * + * @see Information\Value::isEven() + * Use the isEven() method in the Information\Value class instead + * + * @return array|bool|string */ public static function isEven($value = null) { - $value = self::flattenSingleValue($value); - - if ($value === null) { - return self::NAME(); - } elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) { - return self::VALUE(); - } - - return $value % 2 == 0; + return Information\Value::isEven($value); } /** @@ -409,19 +429,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool|string + * @Deprecated 1.23.0 + * + * @see Information\Value::isOdd() + * Use the isOdd() method in the Information\Value class instead + * + * @return array|bool|string */ public static function isOdd($value = null) { - $value = self::flattenSingleValue($value); - - if ($value === null) { - return self::NAME(); - } elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) { - return self::VALUE(); - } - - return abs($value) % 2 == 1; + return Information\Value::isOdd($value); } /** @@ -429,17 +446,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isNumber() + * Use the isNumber() method in the Information\Value class instead + * + * @return array|bool */ public static function isNumber($value = null) { - $value = self::flattenSingleValue($value); - - if (is_string($value)) { - return false; - } - - return is_numeric($value); + return Information\Value::isNumber($value); } /** @@ -447,13 +463,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isLogical() + * Use the isLogical() method in the Information\Value class instead + * + * @return array|bool */ public static function isLogical($value = null) { - $value = self::flattenSingleValue($value); - - return is_bool($value); + return Information\Value::isLogical($value); } /** @@ -461,13 +480,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isText() + * Use the isText() method in the Information\Value class instead + * + * @return array|bool */ public static function isText($value = null) { - $value = self::flattenSingleValue($value); - - return is_string($value) && !self::isError($value); + return Information\Value::isText($value); } /** @@ -475,11 +497,16 @@ class Functions * * @param mixed $value Value to check * - * @return bool + * @Deprecated 1.23.0 + * + * @see Information\Value::isNonText() + * Use the isNonText() method in the Information\Value class instead + * + * @return array|bool */ public static function isNonText($value = null) { - return !self::isText($value); + return Information\Value::isNonText($value); } /** @@ -487,9 +514,14 @@ class Functions * * Returns a value converted to a number * + * @Deprecated 1.23.0 + * + * @see Information\Value::asNumber() + * Use the asNumber() method in the Information\Value class instead + * * @param null|mixed $value The value you want converted * - * @return number N converts values listed in the following table + * @return number|string N converts values listed in the following table * If value is or refers to N returns * A number That number * A date The serial number of that date @@ -500,27 +532,7 @@ class Functions */ public static function n($value = null) { - while (is_array($value)) { - $value = array_shift($value); - } - - switch (gettype($value)) { - case 'double': - case 'float': - case 'integer': - return $value; - case 'boolean': - return (int) $value; - case 'string': - // Errors - if ((strlen($value) > 0) && ($value[0] == '#')) { - return $value; - } - - break; - } - - return 0; + return Information\Value::asNumber($value); } /** @@ -528,6 +540,11 @@ class Functions * * Returns a number that identifies the type of a value * + * @Deprecated 1.23.0 + * + * @see Information\Value::type() + * Use the type() method in the Information\Value class instead + * * @param null|mixed $value The value you want tested * * @return number N converts values listed in the following table @@ -540,39 +557,7 @@ class Functions */ public static function TYPE($value = null) { - $value = self::flattenArrayIndexed($value); - if (is_array($value) && (count($value) > 1)) { - end($value); - $a = key($value); - // Range of cells is an error - if (self::isCellValue($a)) { - return 16; - // Test for Matrix - } elseif (self::isMatrixValue($a)) { - return 64; - } - } elseif (empty($value)) { - // Empty Cell - return 1; - } - $value = self::flattenSingleValue($value); - - if (($value === null) || (is_float($value)) || (is_int($value))) { - return 1; - } elseif (is_bool($value)) { - return 4; - } elseif (is_array($value)) { - return 64; - } elseif (is_string($value)) { - // Errors - if ((strlen($value) > 0) && ($value[0] == '#')) { - return 16; - } - - return 2; - } - - return 0; + return Information\Value::type($value); } /** @@ -588,24 +573,38 @@ class Functions return (array) $array; } - $arrayValues = []; - foreach ($array as $value) { + $flattened = []; + $stack = array_values($array); + + while ($stack) { + $value = array_shift($stack); + if (is_array($value)) { - foreach ($value as $val) { - if (is_array($val)) { - foreach ($val as $v) { - $arrayValues[] = $v; - } - } else { - $arrayValues[] = $val; - } - } + array_unshift($stack, ...array_values($value)); } else { - $arrayValues[] = $value; + $flattened[] = $value; } } - return $arrayValues; + return $flattened; + } + + /** + * @param mixed $value + * + * @return null|mixed + */ + public static function scalar($value) + { + if (!is_array($value)) { + return $value; + } + + do { + $value = array_pop($value); + } while (is_array($value)); + + return $value; } /** @@ -660,29 +659,19 @@ class Functions /** * ISFORMULA. * + * @Deprecated 1.23.0 + * + * @see Information\Value::isFormula() + * Use the isFormula() method in the Information\Value class instead + * * @param mixed $cellReference The cell to check * @param ?Cell $cell The current cell (containing this formula) * - * @return bool|string + * @return array|bool|string */ public static function isFormula($cellReference = '', ?Cell $cell = null) { - if ($cell === null) { - return self::REF(); - } - $cellReference = self::expandDefinedName((string) $cellReference, $cell); - $cellReference = self::trimTrailingRange($cellReference); - - preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellReference, $matches); - - $cellReference = $matches[6] . $matches[7]; - $worksheetName = str_replace("''", "'", trim($matches[2], "'")); - - $worksheet = (!empty($worksheetName)) - ? $cell->getWorksheet()->getParent()->getSheetByName($worksheetName) - : $cell->getWorksheet(); - - return $worksheet->getCell($cellReference)->isFormula(); + return Information\Value::isFormula($cellReference, $cell); } public static function expandDefinedName(string $coordinate, Cell $cell): string @@ -692,12 +681,13 @@ class Functions // Uppercase coordinate $pCoordinatex = strtoupper($coordinate); // Eliminate leading equal sign - $pCoordinatex = Worksheet::pregReplace('/^=/', '', $pCoordinatex); + $pCoordinatex = (string) preg_replace('/^=/', '', $pCoordinatex); $defined = $spreadsheet->getDefinedName($pCoordinatex, $worksheet); if ($defined !== null) { $worksheet2 = $defined->getWorkSheet(); if (!$defined->isFormula() && $worksheet2 !== null) { - $coordinate = "'" . $worksheet2->getTitle() . "'!" . Worksheet::pregReplace('/^=/', '', $defined->getValue()); + $coordinate = "'" . $worksheet2->getTitle() . "'!" . + (string) preg_replace('/^=/', '', str_replace('$', '', $defined->getValue())); } } @@ -706,6 +696,15 @@ class Functions public static function trimTrailingRange(string $coordinate): string { - return Worksheet::pregReplace('/:[\\w\$]+$/', '', $coordinate); + return (string) preg_replace('/:[\\w\$]+$/', '', $coordinate); + } + + public static function trimSheetFromCellReference(string $coordinate): string + { + if (strpos($coordinate, '!') !== false) { + $coordinate = substr($coordinate, strrpos($coordinate, '!') + 1); + } + + return $coordinate; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php new file mode 100644 index 00000000000..4b9f818fe5c --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/ErrorValue.php @@ -0,0 +1,71 @@ + + */ + public const ERROR_CODES = [ + 'null' => '#NULL!', // 1 + 'divisionbyzero' => '#DIV/0!', // 2 + 'value' => '#VALUE!', // 3 + 'reference' => '#REF!', // 4 + 'name' => '#NAME?', // 5 + 'num' => '#NUM!', // 6 + 'na' => '#N/A', // 7 + 'gettingdata' => '#GETTING_DATA', // 8 + 'spill' => '#SPILL!', // 9 + 'connect' => '#CONNECT!', //10 + 'blocked' => '#BLOCKED!', //11 + 'unknown' => '#UNKNOWN!', //12 + 'field' => '#FIELD!', //13 + 'calculation' => '#CALC!', //14 + ]; + + /** + * List of error codes. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use ERROR_CODES constant instead. + * + * @var array + */ + public static $errorCodes = self::ERROR_CODES; + + /** + * @param mixed $value + */ + public static function throwError($value): string + { + return in_array($value, self::ERROR_CODES, true) ? $value : self::ERROR_CODES['value']; + } + + /** + * ERROR_TYPE. + * + * @param mixed $value Value to check + * + * @return array|int|string + */ + public static function type($value = '') + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + $i = 1; + foreach (self::ERROR_CODES as $errorCode) { + if ($value === $errorCode) { + return $i; + } + ++$i; + } + + return self::NA(); + } + + /** + * NULL. + * + * Returns the error value #NULL! + * + * @return string #NULL! + */ + public static function null(): string + { + return self::ERROR_CODES['null']; + } + + /** + * NaN. + * + * Returns the error value #NUM! + * + * @return string #NUM! + */ + public static function NAN(): string + { + return self::ERROR_CODES['num']; + } + + /** + * REF. + * + * Returns the error value #REF! + * + * @return string #REF! + */ + public static function REF(): string + { + return self::ERROR_CODES['reference']; + } + + /** + * NA. + * + * Excel Function: + * =NA() + * + * Returns the error value #N/A + * #N/A is the error value that means "no value is available." + * + * @return string #N/A! + */ + public static function NA(): string + { + return self::ERROR_CODES['na']; + } + + /** + * VALUE. + * + * Returns the error value #VALUE! + * + * @return string #VALUE! + */ + public static function VALUE(): string + { + return self::ERROR_CODES['value']; + } + + /** + * NAME. + * + * Returns the error value #NAME? + * + * @return string #NAME? + */ + public static function NAME(): string + { + return self::ERROR_CODES['name']; + } + + /** + * DIV0. + * + * @return string #DIV/0! + */ + public static function DIV0(): string + { + return self::ERROR_CODES['divisionbyzero']; + } + + /** + * CALC. + * + * @return string #CALC! + */ + public static function CALC(): string + { + return self::ERROR_CODES['calculation']; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php new file mode 100644 index 00000000000..2e524db530b --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Information/Value.php @@ -0,0 +1,328 @@ +getCoordinate()) { + return false; + } + + $cellValue = Functions::trimTrailingRange($value); + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/ui', $cellValue) === 1) { + [$worksheet, $cellValue] = Worksheet::extractSheetTitle($cellValue, true); + if (!empty($worksheet) && $cell->getWorksheet()->getParent()->getSheetByName($worksheet) === null) { + return false; + } + [$column, $row] = Coordinate::indexesFromString($cellValue); + if ($column > 16384 || $row > 1048576) { + return false; + } + + return true; + } + + $namedRange = $cell->getWorksheet()->getParent()->getNamedRange($value); + + return $namedRange instanceof NamedRange; + } + + /** + * IS_EVEN. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isEven($value = null) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if ($value === null) { + return ExcelError::NAME(); + } elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) { + return ExcelError::VALUE(); + } + + return ((int) fmod($value, 2)) === 0; + } + + /** + * IS_ODD. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isOdd($value = null) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if ($value === null) { + return ExcelError::NAME(); + } elseif ((is_bool($value)) || ((is_string($value)) && (!is_numeric($value)))) { + return ExcelError::VALUE(); + } + + return ((int) fmod($value, 2)) !== 0; + } + + /** + * IS_NUMBER. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isNumber($value = null) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + if (is_string($value)) { + return false; + } + + return is_numeric($value); + } + + /** + * IS_LOGICAL. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isLogical($value = null) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return is_bool($value); + } + + /** + * IS_TEXT. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isText($value = null) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return is_string($value) && !ErrorValue::isError($value); + } + + /** + * IS_NONTEXT. + * + * @param mixed $value Value to check + * Or can be an array of values + * + * @return array|bool + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions + */ + public static function isNonText($value = null) + { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + + return !self::isText($value); + } + + /** + * ISFORMULA. + * + * @param mixed $cellReference The cell to check + * @param ?Cell $cell The current cell (containing this formula) + * + * @return array|bool|string + */ + public static function isFormula($cellReference = '', ?Cell $cell = null) + { + if ($cell === null) { + return ExcelError::REF(); + } + + $fullCellReference = Functions::expandDefinedName((string) $cellReference, $cell); + + if (strpos($cellReference, '!') !== false) { + $cellReference = Functions::trimSheetFromCellReference($cellReference); + $cellReferences = Coordinate::extractAllCellReferencesInRange($cellReference); + if (count($cellReferences) > 1) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $cellReferences, $cell); + } + } + + $fullCellReference = Functions::trimTrailingRange($fullCellReference); + + preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $fullCellReference, $matches); + + $fullCellReference = $matches[6] . $matches[7]; + $worksheetName = str_replace("''", "'", trim($matches[2], "'")); + + $worksheet = (!empty($worksheetName)) + ? $cell->getWorksheet()->getParent()->getSheetByName($worksheetName) + : $cell->getWorksheet(); + + return ($worksheet !== null) ? $worksheet->getCell($fullCellReference)->isFormula() : ExcelError::REF(); + } + + /** + * N. + * + * Returns a value converted to a number + * + * @param null|mixed $value The value you want converted + * + * @return number|string N converts values listed in the following table + * If value is or refers to N returns + * A number That number value + * A date The Excel serialized number of that date + * TRUE 1 + * FALSE 0 + * An error value The error value + * Anything else 0 + */ + public static function asNumber($value = null) + { + while (is_array($value)) { + $value = array_shift($value); + } + + switch (gettype($value)) { + case 'double': + case 'float': + case 'integer': + return $value; + case 'boolean': + return (int) $value; + case 'string': + // Errors + if ((strlen($value) > 0) && ($value[0] == '#')) { + return $value; + } + + break; + } + + return 0; + } + + /** + * TYPE. + * + * Returns a number that identifies the type of a value + * + * @param null|mixed $value The value you want tested + * + * @return number N converts values listed in the following table + * If value is or refers to N returns + * A number 1 + * Text 2 + * Logical Value 4 + * An error value 16 + * Array or Matrix 64 + */ + public static function type($value = null) + { + $value = Functions::flattenArrayIndexed($value); + if (is_array($value) && (count($value) > 1)) { + end($value); + $a = key($value); + // Range of cells is an error + if (Functions::isCellValue($a)) { + return 16; + // Test for Matrix + } elseif (Functions::isMatrixValue($a)) { + return 64; + } + } elseif (empty($value)) { + // Empty Cell + return 1; + } + + $value = Functions::flattenSingleValue($value); + if (($value === null) || (is_float($value)) || (is_int($value))) { + return 1; + } elseif (is_bool($value)) { + return 4; + } elseif (is_array($value)) { + return 64; + } elseif (is_string($value)) { + // Errors + if ((strlen($value) > 0) && ($value[0] == '#')) { + return 16; + } + + return 2; + } + + return 0; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical.php index b267e1f0b53..d5d993ae3a1 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical.php @@ -163,7 +163,7 @@ class Logical * * @param mixed $logical A value or expression that can be evaluated to TRUE or FALSE * - * @return bool|string the boolean inverse of the argument + * @return array|bool|string the boolean inverse of the argument */ public static function NOT($logical = false) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php index e84d0f33e4d..6a7757ce23e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Conditional.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Logical; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Calculation\Information\Value; class Conditional { + use ArrayEnabled; + /** * STATEMENT_IF. * @@ -34,21 +40,24 @@ class Conditional * * @param mixed $condition Condition to evaluate * @param mixed $returnIfTrue Value to return when condition is true + * Note that this can be an array value * @param mixed $returnIfFalse Optional value to return when condition is false + * Note that this can be an array value * * @return mixed The value of returnIfTrue or returnIfFalse determined by condition */ public static function statementIf($condition = true, $returnIfTrue = 0, $returnIfFalse = false) { - if (Functions::isError($condition)) { + $condition = ($condition === null) ? true : Functions::flattenSingleValue($condition); + + if (ErrorValue::isError($condition)) { return $condition; } - $condition = ($condition === null) ? true : (bool) Functions::flattenSingleValue($condition); - $returnIfTrue = ($returnIfTrue === null) ? 0 : Functions::flattenSingleValue($returnIfTrue); - $returnIfFalse = ($returnIfFalse === null) ? false : Functions::flattenSingleValue($returnIfFalse); + $returnIfTrue = $returnIfTrue ?? 0; + $returnIfFalse = $returnIfFalse ?? false; - return ($condition) ? $returnIfTrue : $returnIfFalse; + return ((bool) $condition) ? $returnIfTrue : $returnIfFalse; } /** @@ -67,9 +76,11 @@ class Conditional * result1, result2, ... result_n * A list of results. The SWITCH function returns the corresponding result when a value * matches expression. + * Note that these can be array values to be returned * default * Optional. It is the default to return if expression does not match any of the values * (value1, value2, ... value_n). + * Note that this can be an array value to be returned * * @param mixed $arguments Statement arguments * @@ -77,7 +88,7 @@ class Conditional */ public static function statementSwitch(...$arguments) { - $result = Functions::VALUE(); + $result = ExcelError::VALUE(); if (count($arguments) > 0) { $targetValue = Functions::flattenSingleValue($arguments[0]); @@ -99,7 +110,7 @@ class Conditional } if ($switchSatisfied !== true) { - $result = $hasDefaultClause ? $defaultClause : Functions::NA(); + $result = $hasDefaultClause ? $defaultClause : ExcelError::NA(); } } @@ -113,16 +124,23 @@ class Conditional * =IFERROR(testValue,errorpart) * * @param mixed $testValue Value to check, is also the value returned when no error + * Or can be an array of values * @param mixed $errorpart Value to return when testValue is an error condition + * Note that this can be an array value to be returned * * @return mixed The value of errorpart or testValue determined by error condition + * If an array of values is passed as the $testValue argument, then the returned result will also be + * an array with the same dimensions */ public static function IFERROR($testValue = '', $errorpart = '') { - $testValue = ($testValue === null) ? '' : Functions::flattenSingleValue($testValue); - $errorpart = ($errorpart === null) ? '' : Functions::flattenSingleValue($errorpart); + if (is_array($testValue)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $testValue, $errorpart); + } - return self::statementIf(Functions::isError($testValue), $errorpart, $testValue); + $errorpart = $errorpart ?? ''; + + return self::statementIf(ErrorValue::isError($testValue), $errorpart, $testValue); } /** @@ -132,16 +150,23 @@ class Conditional * =IFNA(testValue,napart) * * @param mixed $testValue Value to check, is also the value returned when not an NA + * Or can be an array of values * @param mixed $napart Value to return when testValue is an NA condition + * Note that this can be an array value to be returned * * @return mixed The value of errorpart or testValue determined by error condition + * If an array of values is passed as the $testValue argument, then the returned result will also be + * an array with the same dimensions */ public static function IFNA($testValue = '', $napart = '') { - $testValue = ($testValue === null) ? '' : Functions::flattenSingleValue($testValue); - $napart = ($napart === null) ? '' : Functions::flattenSingleValue($napart); + if (is_array($testValue)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $testValue, $napart); + } - return self::statementIf(Functions::isNa($testValue), $napart, $testValue); + $napart = $napart ?? ''; + + return self::statementIf(ErrorValue::isNa($testValue), $napart, $testValue); } /** @@ -156,6 +181,7 @@ class Conditional * Value returned if corresponding testValue (nth) was true * * @param mixed ...$arguments Statement arguments + * Note that this can be an array value to be returned * * @return mixed|string The value of returnIfTrue_n, if testValue_n was true. #N/A if none of testValues was true */ @@ -164,13 +190,13 @@ class Conditional $argumentCount = count($arguments); if ($argumentCount % 2 != 0) { - return Functions::NA(); + return ExcelError::NA(); } // We use instance of Exception as a falseValue in order to prevent string collision with value in cell $falseValueException = new Exception(); for ($i = 0; $i < $argumentCount; $i += 2) { $testValue = ($arguments[$i] === null) ? '' : Functions::flattenSingleValue($arguments[$i]); - $returnIfTrue = ($arguments[$i + 1] === null) ? '' : Functions::flattenSingleValue($arguments[$i + 1]); + $returnIfTrue = ($arguments[$i + 1] === null) ? '' : $arguments[$i + 1]; $result = self::statementIf($testValue, $returnIfTrue, $falseValueException); if ($result !== $falseValueException) { @@ -178,6 +204,6 @@ class Conditional } } - return Functions::NA(); + return ExcelError::NA(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Operations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Operations.php index 6bfb6a545b6..2e2faa136c7 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Operations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Logical/Operations.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Logical; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Operations { + use ArrayEnabled; + /** * LOGICAL_AND. * @@ -32,7 +36,7 @@ class Operations $args = Functions::flattenArray($args); if (count($args) == 0) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $args = array_filter($args, function ($value) { @@ -73,7 +77,7 @@ class Operations $args = Functions::flattenArray($args); if (count($args) == 0) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $args = array_filter($args, function ($value) { @@ -115,7 +119,7 @@ class Operations $args = Functions::flattenArray($args); if (count($args) == 0) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $args = array_filter($args, function ($value) { @@ -146,12 +150,17 @@ class Operations * holds the value TRUE or FALSE, in which case it is evaluated as the corresponding boolean value * * @param mixed $logical A value or expression that can be evaluated to TRUE or FALSE + * Or can be an array of values * - * @return bool|string the boolean inverse of the argument + * @return array|bool|string the boolean inverse of the argument + * If an array of values is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function NOT($logical = false) { - $logical = Functions::flattenSingleValue($logical); + if (is_array($logical)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $logical); + } if (is_string($logical)) { $logical = mb_strtoupper($logical, 'UTF-8'); @@ -161,7 +170,7 @@ class Operations return true; } - return Functions::VALUE(); + return ExcelError::VALUE(); } return !$logical; @@ -187,7 +196,7 @@ class Operations } elseif (($arg == 'FALSE') || ($arg == Calculation::getFALSE())) { $arg = false; } else { - return Functions::VALUE(); + return ExcelError::VALUE(); } $trueValueCount += ($arg != 0); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php index 67650480fdc..758c4ef017c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -41,9 +41,9 @@ class LookupRef * @param bool $referenceStyle A logical value that specifies the A1 or R1C1 reference style. * TRUE or omitted CELL_ADDRESS returns an A1-style reference * FALSE CELL_ADDRESS returns an R1C1-style reference - * @param string $sheetText Optional Name of worksheet to use + * @param array|string $sheetText Optional Name of worksheet to use * - * @return string + * @return array|string */ public static function cellAddress($row, $column, $relativity = 1, $referenceStyle = true, $sheetText = '') { @@ -277,7 +277,7 @@ class LookupRef * @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|string The relative position of the found item + * @return array|int|string The relative position of the found item */ public static function MATCH($lookupValue, $lookupArray, $matchType = 1) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index 58215f27481..0d2db8b20bc 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; class Address { + use ArrayEnabled; + public const ADDRESS_ABSOLUTE = 1; public const ADDRESS_COLUMN_RELATIVE = 2; public const ADDRESS_ROW_RELATIVE = 3; @@ -24,33 +28,54 @@ class Address * =ADDRESS(row, column, [relativity], [referenceStyle], [sheetText]) * * @param mixed $row Row number (integer) to use in the cell reference + * Or can be an array of values * @param mixed $column Column number (integer) to use in the cell reference + * Or can be an array of values * @param mixed $relativity Integer flag indicating the type of reference to return * 1 or omitted Absolute * 2 Absolute row; relative column * 3 Relative row; absolute column * 4 Relative + * Or can be an array of values * @param mixed $referenceStyle A logical (boolean) value that specifies the A1 or R1C1 reference style. * TRUE or omitted ADDRESS returns an A1-style reference * FALSE ADDRESS returns an R1C1-style reference + * Or can be an array of values * @param mixed $sheetName Optional Name of worksheet to use + * Or can be an array of values * - * @return string + * @return array|string + * If an array of values is passed as the $testValue argument, then the returned result will also be + * an array with the same dimensions */ public static function cell($row, $column, $relativity = 1, $referenceStyle = true, $sheetName = '') { - $row = Functions::flattenSingleValue($row); - $column = Functions::flattenSingleValue($column); - $relativity = ($relativity === null) ? 1 : Functions::flattenSingleValue($relativity); - $referenceStyle = ($referenceStyle === null) ? true : Functions::flattenSingleValue($referenceStyle); - $sheetName = Functions::flattenSingleValue($sheetName); + if ( + is_array($row) || is_array($column) || + is_array($relativity) || is_array($referenceStyle) || is_array($sheetName) + ) { + return self::evaluateArrayArguments( + [self::class, __FUNCTION__], + $row, + $column, + $relativity, + $referenceStyle, + $sheetName + ); + } + + $relativity = $relativity ?? 1; + $referenceStyle = $referenceStyle ?? true; if (($row < 1) || ($column < 1)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $sheetName = self::sheetName($sheetName); + if (is_int($referenceStyle)) { + $referenceStyle = (bool) $referenceStyle; + } if ((!is_bool($referenceStyle)) || $referenceStyle === self::REFERENCE_STYLE_A1) { return self::formatAsA1($row, $column, $relativity, $sheetName); } @@ -92,7 +117,8 @@ class Address if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { $row = "[{$row}]"; } + [$rowChar, $colChar] = AddressHelper::getRowAndColumnChars(); - return "{$sheetName}R{$row}C{$column}"; + return "{$sheetName}$rowChar{$row}$colChar{$column}"; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php index 71358bf30a6..2f691beeff9 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php @@ -2,13 +2,17 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class ExcelMatch { + use ArrayEnabled; + public const MATCHTYPE_SMALLEST_VALUE = -1; public const MATCHTYPE_FIRST_VALUE = 0; public const MATCHTYPE_LARGEST_VALUE = 1; @@ -26,15 +30,16 @@ class ExcelMatch * @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|string The relative position of the found item + * @return array|int|string The relative position of the found item */ public static function MATCH($lookupValue, $lookupArray, $matchType = self::MATCHTYPE_LARGEST_VALUE) { + if (is_array($lookupValue)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType); + } + $lookupArray = Functions::flattenArray($lookupArray); - $lookupValue = Functions::flattenSingleValue($lookupValue); - $matchType = ($matchType === null) - ? self::MATCHTYPE_LARGEST_VALUE - : (int) Functions::flattenSingleValue($matchType); + $matchType = (int) ($matchType ?? self::MATCHTYPE_LARGEST_VALUE); try { // Input validation @@ -79,7 +84,7 @@ class ExcelMatch } // Unsuccessful in finding a match, return #N/A error value - return Functions::NA(); + return ExcelError::NA(); } private static function matchFirstValue($lookupArray, $lookupValue) @@ -149,7 +154,7 @@ class ExcelMatch { // Lookup_value type has to be number, text, or logical values if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) { - throw new Exception(Functions::NA()); + throw new Exception(ExcelError::NA()); } } @@ -160,7 +165,7 @@ class ExcelMatch ($matchType !== self::MATCHTYPE_FIRST_VALUE) && ($matchType !== self::MATCHTYPE_LARGEST_VALUE) && ($matchType !== self::MATCHTYPE_SMALLEST_VALUE) ) { - throw new Exception(Functions::NA()); + throw new Exception(ExcelError::NA()); } } @@ -169,7 +174,7 @@ class ExcelMatch // Lookup_array should not be empty $lookupArraySize = count($lookupArray); if ($lookupArraySize <= 0) { - throw new Exception(Functions::NA()); + throw new Exception(ExcelError::NA()); } } @@ -179,7 +184,7 @@ class ExcelMatch foreach ($lookupArray as $i => $value) { // check the type of the value if ((!is_numeric($value)) && (!is_string($value)) && (!is_bool($value)) && ($value !== null)) { - throw new Exception(Functions::NA()); + throw new Exception(ExcelError::NA()); } // Convert strings to lowercase for case-insensitive testing if (is_string($value)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php new file mode 100644 index 00000000000..74fa8321d0a --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Filter.php @@ -0,0 +1,81 @@ +cellExists($cellReference) || !$worksheet->getCell($cellReference)->isFormula() ) { - return Functions::NA(); + return ExcelError::NA(); } return $worksheet->getCell($cellReference)->getValue(); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php index 7db27804408..e2d27bde241 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -2,13 +2,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class HLookup extends LookupBase { + use ArrayEnabled; + /** * HLOOKUP * The HLOOKUP function searches for value in the top-most row of lookup_array and returns the value @@ -24,12 +27,15 @@ class HLookup extends LookupBase */ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true) { - $lookupValue = Functions::flattenSingleValue($lookupValue); - $indexNumber = Functions::flattenSingleValue($indexNumber); - $notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch); - $lookupArray = self::convertLiteralArray($lookupArray); + if (is_array($lookupValue)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch); + } + + $notExactMatch = (bool) ($notExactMatch ?? true); try { + self::validateLookupArray($lookupArray); + $lookupArray = self::convertLiteralArray($lookupArray); $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); } catch (Exception $e) { return $e->getMessage(); @@ -38,12 +44,12 @@ class HLookup extends LookupBase $f = array_keys($lookupArray); $firstRow = reset($f); if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray))) { - return Functions::REF(); + return ExcelError::REF(); } $firstkey = $f[0] - 1; $returnColumn = $firstkey + $indexNumber; - $firstColumn = array_shift($f); + $firstColumn = array_shift($f) ?? 1; $rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); if ($rowNumber !== null) { @@ -51,24 +57,23 @@ class HLookup extends LookupBase return $lookupArray[$returnColumn][Coordinate::stringFromColumnIndex($rowNumber)]; } - return Functions::NA(); + return ExcelError::NA(); } /** * @param mixed $lookupValue The value that you want to match in lookup_array - * @param mixed $column The column to look up - * @param mixed $notExactMatch determines if you are looking for an exact match based on lookup_value + * @param int|string $column */ - private static function hLookupSearch($lookupValue, array $lookupArray, $column, $notExactMatch): ?int + private static function hLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray[$column] as $rowKey => $rowData) { // break if we have passed possible keys $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData); - $cellDataLower = StringHelper::strToLower($rowData); + $cellDataLower = StringHelper::strToLower((string) $rowData); if ( $notExactMatch && diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php index 28e8df890cb..76a194b38c8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Helpers.php @@ -13,12 +13,12 @@ class Helpers public const CELLADDRESS_USE_R1C1 = false; - private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1): string + private static function convertR1C1(string &$cellAddress1, ?string &$cellAddress2, bool $a1, ?int $baseRow = null, ?int $baseCol = null): string { if ($a1 === self::CELLADDRESS_USE_R1C1) { - $cellAddress1 = AddressHelper::convertToA1($cellAddress1); + $cellAddress1 = AddressHelper::convertToA1($cellAddress1, $baseRow ?? 1, $baseCol ?? 1); if ($cellAddress2) { - $cellAddress2 = AddressHelper::convertToA1($cellAddress2); + $cellAddress2 = AddressHelper::convertToA1($cellAddress2, $baseRow ?? 1, $baseCol ?? 1); } } @@ -35,7 +35,7 @@ class Helpers } } - public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = ''): array + public static function extractCellAddresses(string $cellAddress, bool $a1, Worksheet $sheet, string $sheetName = '', ?int $baseRow = null, ?int $baseCol = null): array { $cellAddress1 = $cellAddress; $cellAddress2 = null; @@ -43,7 +43,7 @@ class Helpers if ($namedRange !== null) { $workSheet = $namedRange->getWorkSheet(); $sheetTitle = ($workSheet === null) ? '' : $workSheet->getTitle(); - $value = preg_replace('/^=/', '', $namedRange->getValue()); + $value = (string) preg_replace('/^=/', '', $namedRange->getValue()); self::adjustSheetTitle($sheetTitle, $value); $cellAddress1 = $sheetTitle . $value; $cellAddress = $cellAddress1; @@ -52,7 +52,7 @@ class Helpers if (strpos($cellAddress, ':') !== false) { [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); } - $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1); + $cellAddress = self::convertR1C1($cellAddress1, $cellAddress2, $a1, $baseRow, $baseCol); return [$cellAddress1, $cellAddress2, $cellAddress]; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php index 1448c089a85..5387833f825 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Hyperlink.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; class Hyperlink @@ -25,7 +26,7 @@ class Hyperlink $displayName = ($displayName === null) ? '' : Functions::flattenSingleValue($displayName); if ((!is_object($cell)) || (trim($linkURL) == '')) { - return Functions::REF(); + return ExcelError::REF(); } if ((is_object($displayName)) || trim($displayName) == '') { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index b5a35410887..91a14491e09 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -5,7 +5,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Indirect @@ -23,7 +25,7 @@ class Indirect return Helpers::CELLADDRESS_USE_A1; } if (is_string($a1fmt)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (bool) $a1fmt; @@ -38,7 +40,7 @@ class Indirect { $cellAddress = Functions::flattenSingleValue($cellAddress); if (!is_string($cellAddress) || !$cellAddress) { - throw new Exception(Functions::REF()); + throw new Exception(ExcelError::REF()); } return $cellAddress; @@ -62,6 +64,8 @@ class Indirect */ public static function INDIRECT($cellAddress, $a1fmt, Cell $cell) { + [$baseCol, $baseRow] = Coordinate::indexesFromString($cell->getCoordinate()); + try { $a1 = self::a1Format($a1fmt); $cellAddress = self::validateAddress($cellAddress); @@ -71,13 +75,23 @@ class Indirect [$cellAddress, $worksheet, $sheetName] = Helpers::extractWorksheet($cellAddress, $cell); - [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName); + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_COLUMNRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_ROWRANGE_RELATIVE . '$/miu', $cellAddress, $matches)) { + $cellAddress = self::handleRowColumnRanges($worksheet, ...explode(':', $cellAddress)); + } + + try { + [$cellAddress1, $cellAddress2, $cellAddress] = Helpers::extractCellAddresses($cellAddress, $a1, $cell->getWorkSheet(), $sheetName, $baseRow, $baseCol); + } catch (Exception $e) { + return ExcelError::REF(); + } if ( - (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) || - (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches))) + (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress1, $matches)) || + (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/miu', $cellAddress2, $matches))) ) { - return Functions::REF(); + return ExcelError::REF(); } return self::extractRequiredCells($worksheet, $cellAddress); @@ -94,4 +108,22 @@ class Indirect return Calculation::getInstance($worksheet !== null ? $worksheet->getParent() : null) ->extractCellRange($cellAddress, $worksheet, false); } + + private static function handleRowColumnRanges(?Worksheet $worksheet, string $start, string $end): string + { + // Being lazy, we're only checking a single row/column to get the max + if (ctype_digit($start) && $start <= 1048576) { + // Max 16,384 columns for Excel2007 + $endColRef = ($worksheet !== null) ? $worksheet->getHighestDataColumn((int) $start) : 'XFD'; + + return "A{$start}:{$endColRef}{$end}"; + } elseif (ctype_alpha($start) && strlen($start) <= 3) { + // Max 1,048,576 rows for Excel2007 + $endRowRef = ($worksheet !== null) ? $worksheet->getHighestDataRow($start) : 1048576; + + return "{$start}1:{$end}{$endRowRef}"; + } + + return "{$start}:{$end}"; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php index e21d35dc56d..76a360b4c19 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; class Lookup { + use ArrayEnabled; + /** * LOOKUP * The LOOKUP function searches for value either from a one-row or one-column range or from an array. @@ -19,10 +22,12 @@ class Lookup */ public static function lookup($lookupValue, $lookupVector, $resultVector = null) { - $lookupValue = Functions::flattenSingleValue($lookupValue); + if (is_array($lookupValue)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $lookupValue, $lookupVector, $resultVector); + } if (!is_array($lookupVector)) { - return Functions::NA(); + return ExcelError::NA(); } $hasResultVector = isset($resultVector); $lookupRows = self::rowCount($lookupVector); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php index 80fc99ad37d..a001540c2bf 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -3,20 +3,38 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; abstract class LookupBase { - protected static function validateIndexLookup($lookup_array, $index_number) + /** + * @param mixed $lookup_array + */ + protected static function validateLookupArray($lookup_array): void { - // index_number must be a number greater than or equal to 1 - if (!is_numeric($index_number) || $index_number < 1) { - throw new Exception(Functions::VALUE()); + if (!is_array($lookup_array)) { + throw new Exception(ExcelError::REF()); + } + } + + protected static function validateIndexLookup(array $lookup_array, $index_number): int + { + // index_number must be a number greater than or equal to 1. + // Excel results are inconsistent when index is non-numeric. + // VLOOKUP(whatever, whatever, SQRT(-1)) yields NUM error, but + // VLOOKUP(whatever, whatever, cellref) yields REF error + // when cellref is '=SQRT(-1)'. So just try our best here. + // Similar results if string (literal yields VALUE, cellRef REF). + if (!is_numeric($index_number)) { + throw new Exception(ExcelError::throwError($index_number)); + } + if ($index_number < 1) { + throw new Exception(ExcelError::VALUE()); } // index_number must be less than or equal to the number of columns in lookup_array if ((!is_array($lookup_array)) || (empty($lookup_array))) { - throw new Exception(Functions::REF()); + throw new Exception(ExcelError::REF()); } return (int) $index_number; @@ -25,7 +43,7 @@ abstract class LookupBase protected static function checkMatch( bool $bothNumeric, bool $bothNotNumeric, - $notExactMatch, + bool $notExactMatch, int $rowKey, string $cellDataLower, string $lookupLower, diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php index b0739eb3000..2282bf476a8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/LookupRefValidations.php @@ -3,7 +3,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class LookupRefValidations { @@ -13,11 +14,11 @@ class LookupRefValidations public static function validateInt($value): int { if (!is_numeric($value)) { - if (Functions::isError($value)) { + if (ErrorValue::isError($value)) { throw new Exception($value); } - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (int) floor((float) $value); @@ -31,7 +32,7 @@ class LookupRefValidations $value = self::validateInt($value); if (($allowZero === false && $value <= 0) || $value < 0) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index 71d22b3d0ff..a447e203c6f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -2,11 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Matrix { + use ArrayEnabled; + + /** + * Helper function; NOT an implementation of any Excel Function. + */ + public static function isColumnVector(array $values): bool + { + return count($values, COUNT_RECURSIVE) === (count($values, COUNT_NORMAL) * 2); + } + + /** + * Helper function; NOT an implementation of any Excel Function. + */ + public static function isRowVector(array $values): bool + { + return count($values, COUNT_RECURSIVE) > 1 && + (count($values, COUNT_NORMAL) === 1 || count($values, COUNT_RECURSIVE) === count($values, COUNT_NORMAL)); + } + /** * TRANSPOSE. * @@ -45,17 +65,25 @@ class Matrix * @param mixed $matrix A range of cells or an array constant * @param mixed $rowNum The row in the array or range from which to return a value. * If row_num is omitted, column_num is required. + * Or can be an array of values * @param mixed $columnNum The column in the array or range from which to return a value. * If column_num is omitted, row_num is required. + * Or can be an array of values * * TODO Provide support for area_num, currently not supported * * @return mixed the value of a specified cell or array of cells + * If an array of values is passed as the $rowNum and/or $columnNum arguments, then the returned result + * will also be an array with the same dimensions */ public static function index($matrix, $rowNum = 0, $columnNum = 0) { - $rowNum = ($rowNum === null) ? 0 : Functions::flattenSingleValue($rowNum); - $columnNum = ($columnNum === null) ? 0 : Functions::flattenSingleValue($columnNum); + if (is_array($rowNum) || is_array($columnNum)) { + return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $matrix, $rowNum, $columnNum); + } + + $rowNum = $rowNum ?? 0; + $columnNum = $columnNum ?? 0; try { $rowNum = LookupRefValidations::validatePositiveInt($rowNum); @@ -65,14 +93,14 @@ class Matrix } if (!is_array($matrix) || ($rowNum > count($matrix))) { - return Functions::REF(); + return ExcelError::REF(); } $rowKeys = array_keys($matrix); $columnKeys = @array_keys($matrix[$rowKeys[0]]); if ($columnNum > count($columnKeys)) { - return Functions::REF(); + return ExcelError::REF(); } if ($columnNum === 0) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php index 7e33f55acaf..02a255812fb 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -48,11 +49,11 @@ class Offset $width = Functions::flattenSingleValue($width); if ($cellAddress === null || $cellAddress === '') { - return Functions::VALUE(); + return ExcelError::VALUE(); } if (!is_object($cell)) { - return Functions::REF(); + return ExcelError::REF(); } [$cellAddress, $worksheet] = self::extractWorksheet($cellAddress, $cell); @@ -69,7 +70,7 @@ class Offset $startCellColumn += $columns; if (($startCellRow <= 0) || ($startCellColumn < 0)) { - return Functions::REF(); + return ExcelError::REF(); } $endCellColumn = self::adjustEndCellColumnForWidth($endCellColumn, $width, $startCellColumn, $columns); @@ -78,7 +79,7 @@ class Offset $endCellRow = self::adustEndCellRowForHeight($height, $startCellRow, $rows, $endCellRow); if (($endCellRow <= 0) || ($endCellColumn < 0)) { - return Functions::REF(); + return ExcelError::REF(); } $endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1); @@ -98,6 +99,8 @@ class Offset private static function extractWorksheet($cellAddress, Cell $cell): array { + $cellAddress = self::assessCellAddress($cellAddress, $cell); + $sheetName = ''; if (strpos($cellAddress, '!') !== false) { [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); @@ -111,6 +114,15 @@ class Offset return [$cellAddress, $worksheet]; } + private static function assessCellAddress(string $cellAddress, Cell $cell): string + { + if (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $cellAddress) !== false) { + $cellAddress = Functions::expandDefinedName($cellAddress, $cell); + } + + return $cellAddress; + } + private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns) { $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php index 9752d6730aa..8bce07e9086 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -50,7 +50,7 @@ class RowColumnInformation if (is_array($cellAddress)) { foreach ($cellAddress as $columnKey => $value) { - $columnKey = preg_replace('/[^a-z]/i', '', $columnKey); + $columnKey = (string) preg_replace('/[^a-z]/i', '', $columnKey); return (int) Coordinate::columnIndexFromString($columnKey); } @@ -66,8 +66,8 @@ class RowColumnInformation [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/[^a-z]/i', '', $startAddress); - $endAddress = preg_replace('/[^a-z]/i', '', $endAddress); + $startAddress = (string) preg_replace('/[^a-z]/i', '', $startAddress); + $endAddress = (string) preg_replace('/[^a-z]/i', '', $endAddress); return range( (int) Coordinate::columnIndexFromString($startAddress), @@ -75,7 +75,7 @@ class RowColumnInformation ); } - $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); + $cellAddress = (string) preg_replace('/[^a-z]/i', '', $cellAddress); return (int) Coordinate::columnIndexFromString($cellAddress); } @@ -99,7 +99,7 @@ class RowColumnInformation return 1; } if (!is_array($cellAddress)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } reset($cellAddress); @@ -159,8 +159,8 @@ class RowColumnInformation [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); if (strpos($cellAddress, ':') !== false) { [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/\D/', '', $startAddress); - $endAddress = preg_replace('/\D/', '', $endAddress); + $startAddress = (string) preg_replace('/\D/', '', $startAddress); + $endAddress = (string) preg_replace('/\D/', '', $endAddress); return array_map( function ($value) { @@ -193,7 +193,7 @@ class RowColumnInformation return 1; } if (!is_array($cellAddress)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } reset($cellAddress); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php index 6c18d73b83e..0ac917773bc 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Selection.php @@ -2,10 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Selection { + use ArrayEnabled; + /** * CHOOSE. * @@ -15,26 +19,27 @@ class Selection * Excel Function: * =CHOOSE(index_num, value1, [value2], ...) * + * @param mixed $chosenEntry The entry to select from the list (indexed from 1) * @param mixed ...$chooseArgs Data values * * @return mixed The selected value */ - public static function choose(...$chooseArgs) + public static function choose($chosenEntry, ...$chooseArgs) { - $chosenEntry = Functions::flattenArray(array_shift($chooseArgs)); + if (is_array($chosenEntry)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $chosenEntry, ...$chooseArgs); + } + $entryCount = count($chooseArgs) - 1; - if (is_array($chosenEntry)) { - $chosenEntry = array_shift($chosenEntry); - } if (is_numeric($chosenEntry)) { --$chosenEntry; } else { - return Functions::VALUE(); + return ExcelError::VALUE(); } $chosenEntry = floor($chosenEntry); if (($chosenEntry < 0) || ($chosenEntry > $entryCount)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } if (is_array($chooseArgs[$chosenEntry])) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php new file mode 100644 index 00000000000..ff78fbea867 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Sort.php @@ -0,0 +1,342 @@ +getMessage(); + } + + // We want a simple, enumrated array of arrays where we can reference column by its index number. + $sortArray = array_values(array_map('array_values', $sortArray)); + + return ($byColumn === true) + ? self::sortByColumn($sortArray, $sortIndex, $sortOrder) + : self::sortByRow($sortArray, $sortIndex, $sortOrder); + } + + /** + * SORTBY + * The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array. + * The returned array is the same shape as the provided array argument. + * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting. + * + * @param mixed $sortArray The range of cells being sorted + * @param mixed $args + * At least one additional argument must be provided, The vector or range to sort on + * After that, arguments are passed as pairs: + * sort order: ascending or descending + * Ascending = 1 (self::ORDER_ASCENDING) + * Descending = -1 (self::ORDER_DESCENDING) + * additional arrays or ranges for multi-level sorting + * + * @return mixed The sorted values from the sort range + */ + public static function sortBy($sortArray, ...$args) + { + if (!is_array($sortArray)) { + // Scalars are always returned "as is" + return $sortArray; + } + + $sortArray = self::enumerateArrayKeys($sortArray); + + $lookupArraySize = count($sortArray); + $argumentCount = count($args); + + try { + $sortBy = $sortOrder = []; + for ($i = 0; $i < $argumentCount; $i += 2) { + $sortBy[] = self::validateSortVector($args[$i], $lookupArraySize); + $sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::processSortBy($sortArray, $sortBy, $sortOrder); + } + + private static function enumerateArrayKeys(array $sortArray): array + { + array_walk( + $sortArray, + function (&$columns): void { + if (is_array($columns)) { + $columns = array_values($columns); + } + } + ); + + return array_values($sortArray); + } + + /** + * @param mixed $sortIndex + * @param mixed $sortOrder + */ + private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void + { + if (is_array($sortIndex) || is_array($sortOrder)) { + throw new Exception(ExcelError::VALUE()); + } + + $sortIndex = self::validatePositiveInt($sortIndex, false); + + if ($sortIndex > $sortArraySize) { + throw new Exception(ExcelError::VALUE()); + } + + $sortOrder = self::validateSortOrder($sortOrder); + } + + /** + * @param mixed $sortVector + */ + private static function validateSortVector($sortVector, int $sortArraySize): array + { + if (!is_array($sortVector)) { + throw new Exception(ExcelError::VALUE()); + } + + // It doesn't matter if it's a row or a column vectors, it works either way + $sortVector = Functions::flattenArray($sortVector); + if (count($sortVector) !== $sortArraySize) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortVector; + } + + /** + * @param mixed $sortOrder + */ + private static function validateSortOrder($sortOrder): int + { + $sortOrder = self::validateInt($sortOrder); + if (($sortOrder == self::ORDER_ASCENDING || $sortOrder === self::ORDER_DESCENDING) === false) { + throw new Exception(ExcelError::VALUE()); + } + + return $sortOrder; + } + + /** + * @param array $sortIndex + * @param mixed $sortOrder + */ + private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void + { + // It doesn't matter if they're row or column vectors, it works either way + $sortIndex = Functions::flattenArray($sortIndex); + $sortOrder = Functions::flattenArray($sortOrder); + + if ( + count($sortOrder) === 0 || count($sortOrder) > $sortArraySize || + (count($sortOrder) > count($sortIndex)) + ) { + throw new Exception(ExcelError::VALUE()); + } + + if (count($sortIndex) > count($sortOrder)) { + // If $sortOrder has fewer elements than $sortIndex, then the last order element is repeated. + $sortOrder = array_merge( + $sortOrder, + array_fill(0, count($sortIndex) - count($sortOrder), array_pop($sortOrder)) + ); + } + + foreach ($sortIndex as $key => &$value) { + self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize); + } + } + + private static function prepareSortVectorValues(array $sortVector): array + { + // Strings should be sorted case-insensitive; with booleans converted to locale-strings + return array_map( + function ($value) { + if (is_bool($value)) { + return ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); + } elseif (is_string($value)) { + return StringHelper::strToLower($value); + } + + return $value; + }, + $sortVector + ); + } + + /** + * @param array[] $sortIndex + * @param int[] $sortOrder + */ + private static function processSortBy(array $sortArray, array $sortIndex, $sortOrder): array + { + $sortArguments = []; + $sortData = []; + foreach ($sortIndex as $index => $sortValues) { + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments); + + $sortVector = self::executeVectorSortQuery($sortData, $sortArguments); + + return self::sortLookupArrayFromVector($sortArray, $sortVector); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder); + + return self::sortLookupArrayFromVector($sortArray, $sortVector); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortArray = Matrix::transpose($sortArray); + $result = self::sortByRow($sortArray, $sortIndex, $sortOrder); + + return Matrix::transpose($result); + } + + /** + * @param int[] $sortIndex + * @param int[] $sortOrder + */ + private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array + { + $sortArguments = []; + $sortData = []; + foreach ($sortIndex as $index => $sortIndexValue) { + $sortValues = array_column($sortArray, $sortIndexValue - 1); + $sortData[] = $sortValues; + $sortArguments[] = self::prepareSortVectorValues($sortValues); + $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC; + } + $sortArguments = self::applyPHP7Patch($sortArray, $sortArguments); + + $sortData = self::executeVectorSortQuery($sortData, $sortArguments); + + return $sortData; + } + + private static function executeVectorSortQuery(array $sortData, array $sortArguments): array + { + $sortData = Matrix::transpose($sortData); + + // We need to set an index that can be retained, as array_multisort doesn't maintain numeric keys. + $sortDataIndexed = []; + foreach ($sortData as $key => $value) { + $sortDataIndexed[Coordinate::stringFromColumnIndex($key + 1)] = $value; + } + unset($sortData); + + $sortArguments[] = &$sortDataIndexed; + + array_multisort(...$sortArguments); + + // After the sort, we restore the numeric keys that will now be in the correct, sorted order + $sortedData = []; + foreach (array_keys($sortDataIndexed) as $key) { + $sortedData[] = Coordinate::columnIndexFromString($key) - 1; + } + + return $sortedData; + } + + private static function sortLookupArrayFromVector(array $sortArray, array $sortVector): array + { + // Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays + $sortedArray = []; + foreach ($sortVector as $index) { + $sortedArray[] = $sortArray[$index]; + } + + return $sortedArray; + +// uksort( +// $lookupArray, +// function (int $a, int $b) use (array $sortVector) { +// return $sortVector[$a] <=> $sortVector[$b]; +// } +// ); +// +// return $lookupArray; + } + + /** + * Hack to handle PHP 7: + * From PHP 8.0.0, If two members compare as equal in a sort, they retain their original order; + * but prior to PHP 8.0.0, their relative order in the sorted array was undefined. + * MS Excel replicates the PHP 8.0.0 behaviour, retaining the original order of matching elements. + * To replicate that behaviour with PHP 7, we add an extra sort based on the row index. + */ + private static function applyPHP7Patch(array $sortArray, array $sortArguments): array + { + if (PHP_VERSION_ID < 80000) { + $sortArguments[] = range(1, count($sortArray)); + $sortArguments[] = SORT_ASC; + } + + return $sortArguments; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php new file mode 100644 index 00000000000..2ba51281eae --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/Unique.php @@ -0,0 +1,141 @@ + count($flattenedLookupVector, COUNT_RECURSIVE) + 1) { + // We're looking at a full column check (multiple rows) + $transpose = Matrix::transpose($lookupVector); + $result = self::uniqueByRow($transpose, $exactlyOnce); + + return (is_array($result)) ? Matrix::transpose($result) : $result; + } + + $result = self::countValuesCaseInsensitive($flattenedLookupVector); + + if ($exactlyOnce === true) { + $result = self::exactlyOnceFilter($result); + } + + if (count($result) === 0) { + return ExcelError::CALC(); + } + + $result = array_keys($result); + + return $result; + } + + private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array + { + $caseInsensitiveCounts = array_count_values( + array_map( + function (string $value) { + return StringHelper::strToUpper($value); + }, + $caseSensitiveLookupValues + ) + ); + + $caseSensitiveCounts = []; + foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) { + if (is_numeric($caseInsensitiveKey)) { + $caseSensitiveCounts[$caseInsensitiveKey] = $count; + } else { + foreach ($caseSensitiveLookupValues as $caseSensitiveValue) { + if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) { + $caseSensitiveCounts[$caseSensitiveValue] = $count; + + break; + } + } + } + } + + return $caseSensitiveCounts; + } + + private static function exactlyOnceFilter(array $values): array + { + return array_filter( + $values, + function ($value) { + return $value === 1; + } + ); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php index ddd5d9ee362..edeb1aa8cc2 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class VLookup extends LookupBase { + use ArrayEnabled; + /** * VLOOKUP * The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value @@ -23,11 +26,14 @@ class VLookup extends LookupBase */ public static function lookup($lookupValue, $lookupArray, $indexNumber, $notExactMatch = true) { - $lookupValue = Functions::flattenSingleValue($lookupValue); - $indexNumber = Functions::flattenSingleValue($indexNumber); - $notExactMatch = ($notExactMatch === null) ? true : Functions::flattenSingleValue($notExactMatch); + if (is_array($lookupValue)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $indexNumber, $notExactMatch); + } + + $notExactMatch = (bool) ($notExactMatch ?? true); try { + self::validateLookupArray($lookupArray); $indexNumber = self::validateIndexLookup($lookupArray, $indexNumber); } catch (Exception $e) { return $e->getMessage(); @@ -36,14 +42,16 @@ class VLookup extends LookupBase $f = array_keys($lookupArray); $firstRow = array_pop($f); if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray[$firstRow]))) { - return Functions::REF(); + return ExcelError::REF(); } $columnKeys = array_keys($lookupArray[$firstRow]); $returnColumn = $columnKeys[--$indexNumber]; - $firstColumn = array_shift($columnKeys); + $firstColumn = array_shift($columnKeys) ?? 1; if (!$notExactMatch) { - uasort($lookupArray, ['self', 'vlookupSort']); + /** @var callable */ + $callable = [self::class, 'vlookupSort']; + uasort($lookupArray, $callable); } $rowNumber = self::vLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); @@ -53,15 +61,15 @@ class VLookup extends LookupBase return $lookupArray[$rowNumber][$returnColumn]; } - return Functions::NA(); + return ExcelError::NA(); } - private static function vlookupSort($a, $b) + private static function vlookupSort(array $a, array $b): int { reset($a); $firstColumn = key($a); - $aLower = StringHelper::strToLower($a[$firstColumn]); - $bLower = StringHelper::strToLower($b[$firstColumn]); + $aLower = StringHelper::strToLower((string) $a[$firstColumn]); + $bLower = StringHelper::strToLower((string) $b[$firstColumn]); if ($aLower == $bLower) { return 0; @@ -70,15 +78,19 @@ class VLookup extends LookupBase return ($aLower < $bLower) ? -1 : 1; } - private static function vLookupSearch($lookupValue, $lookupArray, $column, $notExactMatch) + /** + * @param mixed $lookupValue The value that you want to match in lookup_array + * @param int|string $column + */ + private static function vLookupSearch($lookupValue, array $lookupArray, $column, bool $notExactMatch): ?int { - $lookupLower = StringHelper::strToLower($lookupValue); + $lookupLower = StringHelper::strToLower((string) $lookupValue); $rowNumber = null; foreach ($lookupArray as $rowKey => $rowData) { $bothNumeric = is_numeric($lookupValue) && is_numeric($rowData[$column]); $bothNotNumeric = !is_numeric($lookupValue) && !is_numeric($rowData[$column]); - $cellDataLower = StringHelper::strToLower($rowData[$column]); + $cellDataLower = StringHelper::strToLower((string) $rowData[$column]); // break if we have passed possible keys if ( diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php index ec251f6d25e..993154c33c0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -20,9 +20,9 @@ class MathTrig * @See MathTrig\Arabic::evaluate() * Use the evaluate method in the MathTrig\Arabic class instead * - * @param string $roman + * @param array|string $roman * - * @return int|string the arabic numberal contrived from the roman numeral + * @return array|int|string the arabic numberal contrived from the roman numeral */ public static function ARABIC($roman) { @@ -50,10 +50,10 @@ class MathTrig * @See MathTrig\Trig\Tangent::atan2() * Use the atan2 method in the MathTrig\Trig\Tangent class instead * - * @param float $xCoordinate the x-coordinate of the point - * @param float $yCoordinate the y-coordinate of the point + * @param array|float $xCoordinate the x-coordinate of the point + * @param array|float $yCoordinate the y-coordinate of the point * - * @return float|string the inverse tangent of the specified x- and y-coordinates, or a string containing an error + * @return array|float|string the inverse tangent of the specified x- and y-coordinates, or a string containing an error */ public static function ATAN2($xCoordinate = null, $yCoordinate = null) { @@ -77,7 +77,7 @@ class MathTrig * @param float $radix * @param int $minLength * - * @return string the text representation with the given radix (base) + * @return array|string the text representation with the given radix (base) */ public static function BASE($number, $radix, $minLength = null) { @@ -100,7 +100,7 @@ class MathTrig * @param float $number the number you want to round * @param float $significance the multiple to which you want to round * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error * * @see MathTrig\Ceiling::ceiling() * Use the ceiling() method in the MathTrig\Ceiling class instead @@ -124,10 +124,10 @@ class MathTrig * @see MathTrig\Combinations::withoutRepetition() * Use the withoutRepetition() method in the MathTrig\Combinations class instead * - * @param int $numObjs Number of different objects - * @param int $numInSet Number of objects in each combination + * @param array|int $numObjs Number of different objects + * @param array|int $numInSet Number of objects in each combination * - * @return float|int|string Number of combinations, or a string containing an error + * @return array|float|int|string Number of combinations, or a string containing an error */ public static function COMBIN($numObjs, $numInSet) { @@ -151,9 +151,9 @@ class MathTrig * @see MathTrig\Round::even() * Use the even() method in the MathTrig\Round class instead * - * @param float $number Number to round + * @param array|float $number Number to round * - * @return float|int|string Rounded Number, or a string containing an error + * @return array|float|int|string Rounded Number, or a string containing an error */ public static function EVEN($number) { @@ -184,9 +184,9 @@ class MathTrig * * @Deprecated 1.18.0 * - * @param float $factVal Factorial Value + * @param array|float $factVal Factorial Value * - * @return float|int|string Factorial, or a string containing an error + * @return array|float|int|string Factorial, or a string containing an error * *@see MathTrig\Factorial::fact() * Use the fact() method in the MathTrig\Factorial class instead @@ -206,9 +206,9 @@ class MathTrig * * @Deprecated 1.18.0 * - * @param float $factVal Factorial Value + * @param array|float $factVal Factorial Value * - * @return float|int|string Double Factorial, or a string containing an error + * @return array|float|int|string Double Factorial, or a string containing an error * *@see MathTrig\Factorial::factDouble() * Use the factDouble() method in the MathTrig\Factorial class instead @@ -231,7 +231,7 @@ class MathTrig * @param float $number Number to round * @param float $significance Significance * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error * *@see MathTrig\Floor::floor() * Use the floor() method in the MathTrig\Floor class instead @@ -255,7 +255,7 @@ class MathTrig * @param float $significance Significance * @param int $mode direction to round negative numbers * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error * *@see MathTrig\Floor::math() * Use the math() method in the MathTrig\Floor class instead @@ -278,7 +278,7 @@ class MathTrig * @param float $number Number to round * @param float $significance Significance * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error * *@see MathTrig\Floor::precise() * Use the precise() method in the MathTrig\Floor class instead @@ -301,9 +301,9 @@ class MathTrig * @see MathTrig\IntClass::evaluate() * Use the evaluate() method in the MathTrig\IntClass class instead * - * @param float $number Number to cast to an integer + * @param array|float $number Number to cast to an integer * - * @return int|string Integer value, or a string containing an error + * @return array|int|string Integer value, or a string containing an error */ public static function INT($number) { @@ -375,7 +375,7 @@ class MathTrig * @param float $number The positive real number for which you want the logarithm * @param float $base The base of the logarithm. If base is omitted, it is assumed to be 10. * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function logBase($number, $base = 10) { @@ -455,7 +455,7 @@ class MathTrig * @param int $a Dividend * @param int $b Divisor * - * @return float|int|string Remainder, or a string containing an error + * @return array|float|int|string Remainder, or a string containing an error */ public static function MOD($a = 1, $b = 1) { @@ -470,9 +470,9 @@ class MathTrig * @Deprecated 1.17.0 * * @param float $number Number to round - * @param int $multiple Multiple to which you want to round $number + * @param array|int $multiple Multiple to which you want to round $number * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error * *@see MathTrig\Round::multiple() * Use the multiple() method in the MathTrig\Mround class instead @@ -511,9 +511,9 @@ class MathTrig * @See MathTrig\Round::odd() * Use the odd method in the MathTrig\Round class instead * - * @param float $number Number to round + * @param array|float $number Number to round * - * @return float|int|string Rounded Number, or a string containing an error + * @return array|float|int|string Rounded Number, or a string containing an error */ public static function ODD($number) { @@ -533,7 +533,7 @@ class MathTrig * @param float $x * @param float $y * - * @return float|int|string The result, or a string containing an error + * @return array|float|int|string The result, or a string containing an error */ public static function POWER($x = 0, $y = 2) { @@ -579,7 +579,7 @@ class MathTrig * @param mixed $numerator * @param mixed $denominator * - * @return int|string + * @return array|int|string */ public static function QUOTIENT($numerator, $denominator) { @@ -597,7 +597,7 @@ class MathTrig * @param int $min Minimal value * @param int $max Maximal value * - * @return float|int|string Random number + * @return array|float|int|string Random number */ public static function RAND($min = 0, $max = 0) { @@ -617,7 +617,7 @@ class MathTrig * @param mixed $aValue Number to convert * @param mixed $style Number indicating one of five possible forms * - * @return string Roman numeral, or a string containing an error + * @return array|string Roman numeral, or a string containing an error */ public static function ROMAN($aValue, $style = 0) { @@ -634,10 +634,10 @@ class MathTrig * @See MathTrig\Round::up() * Use the up() method in the MathTrig\Round class instead * - * @param float $number Number to round - * @param int $digits Number of digits to which you want to round $number + * @param array|float $number Number to round + * @param array|int $digits Number of digits to which you want to round $number * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error */ public static function ROUNDUP($number, $digits) { @@ -654,10 +654,10 @@ class MathTrig * @See MathTrig\Round::down() * Use the down() method in the MathTrig\Round class instead * - * @param float $number Number to round - * @param int $digits Number of digits to which you want to round $number + * @param array|float $number Number to round + * @param array|int $digits Number of digits to which you want to round $number * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error */ public static function ROUNDDOWN($number, $digits) { @@ -679,7 +679,7 @@ class MathTrig * @param mixed $m Step * @param mixed[] $args An array of coefficients for the Data Series * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function SERIESSUM($x, $n, $m, ...$args) { @@ -697,9 +697,9 @@ class MathTrig * @See MathTrig\Sign::evaluate() * Use the evaluate method in the MathTrig\Sign class instead * - * @param float $number Number to round + * @param array|float $number Number to round * - * @return int|string sign value, or a string containing an error + * @return array|int|string sign value, or a string containing an error */ public static function SIGN($number) { @@ -729,9 +729,9 @@ class MathTrig * @See MathTrig\Sqrt::sqrt() * Use the pi method in the MathTrig\Sqrt class instead * - * @param float $number Number + * @param array|float $number Number * - * @return float|string Square Root of Number * Pi, or a string containing an error + * @return array|float|string Square Root of Number * Pi, or a string containing an error */ public static function SQRTPI($number) { @@ -941,7 +941,7 @@ class MathTrig * @param float $value * @param int $digits * - * @return float|string Truncated value, or a string containing an error + * @return array|float|string Truncated value, or a string containing an error */ public static function TRUNC($value = 0, $digits = 0) { @@ -958,9 +958,9 @@ class MathTrig * @See MathTrig\Trig\Secant::sec() * Use the sec method in the MathTrig\Trig\Secant class instead * - * @param float $angle Number + * @param array|float $angle Number * - * @return float|string The secant of the angle + * @return array|float|string The secant of the angle */ public static function SEC($angle) { @@ -977,9 +977,9 @@ class MathTrig * @See MathTrig\Trig\Secant::sech() * Use the sech method in the MathTrig\Trig\Secant class instead * - * @param float $angle Number + * @param array|float $angle Number * - * @return float|string The hyperbolic secant of the angle + * @return array|float|string The hyperbolic secant of the angle */ public static function SECH($angle) { @@ -996,9 +996,9 @@ class MathTrig * @See MathTrig\Trig\Cosecant::csc() * Use the csc method in the MathTrig\Trig\Cosecant class instead * - * @param float $angle Number + * @param array|float $angle Number * - * @return float|string The cosecant of the angle + * @return array|float|string The cosecant of the angle */ public static function CSC($angle) { @@ -1015,9 +1015,9 @@ class MathTrig * @See MathTrig\Trig\Cosecant::csch() * Use the csch method in the MathTrig\Trig\Cosecant class instead * - * @param float $angle Number + * @param array|float $angle Number * - * @return float|string The hyperbolic cosecant of the angle + * @return array|float|string The hyperbolic cosecant of the angle */ public static function CSCH($angle) { @@ -1034,9 +1034,9 @@ class MathTrig * @See MathTrig\Trig\Cotangent::cot() * Use the cot method in the MathTrig\Trig\Cotangent class instead * - * @param float $angle Number + * @param array|float $angle Number * - * @return float|string The cotangent of the angle + * @return array|float|string The cotangent of the angle */ public static function COT($angle) { @@ -1053,9 +1053,9 @@ class MathTrig * @See MathTrig\Trig\Cotangent::coth() * Use the coth method in the MathTrig\Trig\Cotangent class instead * - * @param float $angle Number + * @param array|float $angle Number * - * @return float|string The hyperbolic cotangent of the angle + * @return array|float|string The hyperbolic cotangent of the angle */ public static function COTH($angle) { @@ -1072,9 +1072,9 @@ class MathTrig * @See MathTrig\Trig\Cotangent::acot() * Use the acot method in the MathTrig\Trig\Cotangent class instead * - * @param float $number Number + * @param array|float $number Number * - * @return float|string The arccotangent of the number + * @return array|float|string The arccotangent of the number */ public static function ACOT($number) { @@ -1108,9 +1108,9 @@ class MathTrig * @See MathTrig\Trig\Cotangent::acoth() * Use the acoth method in the MathTrig\Trig\Cotangent class instead * - * @param float $number Number + * @param array|float $number Number * - * @return float|string The hyperbolic arccotangent of the number + * @return array|float|string The hyperbolic arccotangent of the number */ public static function ACOTH($number) { @@ -1127,10 +1127,10 @@ class MathTrig * @See MathTrig\Round::round() * Use the round() method in the MathTrig\Round class instead * - * @param mixed $number Should be numeric - * @param mixed $precision Should be int + * @param array|mixed $number Should be numeric + * @param array|mixed $precision Should be int * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinROUND($number, $precision) { @@ -1147,9 +1147,9 @@ class MathTrig * @See MathTrig\Absolute::evaluate() * Use the evaluate method in the MathTrig\Absolute class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|int|string Rounded number + * @return array|float|int|string Rounded number */ public static function builtinABS($number) { @@ -1166,9 +1166,9 @@ class MathTrig * * Returns the result of builtin function acos after validating args. * - * @param mixed $number Should be numeric + * @param array|float $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinACOS($number) { @@ -1185,9 +1185,9 @@ class MathTrig * @See MathTrig\Trig\Cosine::acosh() * Use the acosh method in the MathTrig\Trig\Cosine class instead * - * @param mixed $number Should be numeric + * @param array|float $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinACOSH($number) { @@ -1204,9 +1204,9 @@ class MathTrig * @See MathTrig\Trig\Sine::asin() * Use the asin method in the MathTrig\Trig\Sine class instead * - * @param mixed $number Should be numeric + * @param array|float $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinASIN($number) { @@ -1223,9 +1223,9 @@ class MathTrig * @See MathTrig\Trig\Sine::asinh() * Use the asinh method in the MathTrig\Trig\Sine class instead * - * @param mixed $number Should be numeric + * @param array|float $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinASINH($number) { @@ -1242,9 +1242,9 @@ class MathTrig * @See MathTrig\Trig\Tangent::atan() * Use the atan method in the MathTrig\Trig\Tangent class instead * - * @param mixed $number Should be numeric + * @param array|float $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinATAN($number) { @@ -1261,9 +1261,9 @@ class MathTrig * @See MathTrig\Trig\Tangent::atanh() * Use the atanh method in the MathTrig\Trig\Tangent class instead * - * @param mixed $number Should be numeric + * @param array|float $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinATANH($number) { @@ -1280,9 +1280,9 @@ class MathTrig * @See MathTrig\Trig\Cosine::cos() * Use the cos method in the MathTrig\Trig\Cosine class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinCOS($number) { @@ -1299,9 +1299,9 @@ class MathTrig * @See MathTrig\Trig\Cosine::cosh() * Use the cosh method in the MathTrig\Trig\Cosine class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinCOSH($number) { @@ -1318,9 +1318,9 @@ class MathTrig * @See MathTrig\Angle::toDegrees() * Use the toDegrees method in the MathTrig\Angle class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinDEGREES($number) { @@ -1337,9 +1337,9 @@ class MathTrig * @See MathTrig\Exp::evaluate() * Use the evaluate method in the MathTrig\Exp class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinEXP($number) { @@ -1358,7 +1358,7 @@ class MathTrig * * @param mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinLN($number) { @@ -1377,7 +1377,7 @@ class MathTrig * * @param mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinLOG10($number) { @@ -1394,9 +1394,9 @@ class MathTrig * @See MathTrig\Angle::toRadians() * Use the toRadians method in the MathTrig\Angle class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinRADIANS($number) { @@ -1413,9 +1413,9 @@ class MathTrig * @See MathTrig\Trig\Sine::evaluate() * Use the sin method in the MathTrig\Trig\Sine class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string sine + * @return array|float|string sine */ public static function builtinSIN($number) { @@ -1432,9 +1432,9 @@ class MathTrig * @See MathTrig\Trig\Sine::sinh() * Use the sinh method in the MathTrig\Trig\Sine class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinSINH($number) { @@ -1451,9 +1451,9 @@ class MathTrig * @See MathTrig\Sqrt::sqrt() * Use the sqrt method in the MathTrig\Sqrt class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinSQRT($number) { @@ -1470,9 +1470,9 @@ class MathTrig * @See MathTrig\Trig\Tangent::tan() * Use the tan method in the MathTrig\Trig\Tangent class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinTAN($number) { @@ -1489,9 +1489,9 @@ class MathTrig * @See MathTrig\Trig\Tangent::tanh() * Use the tanh method in the MathTrig\Trig\Tangent class instead * - * @param mixed $number Should be numeric + * @param array|mixed $number Should be numeric * - * @return float|string Rounded number + * @return array|float|string Rounded number */ public static function builtinTANH($number) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php index 9f1bd804970..f21c6b73cac 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php @@ -2,21 +2,30 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Absolute { + use ArrayEnabled; + /** * ABS. * * Returns the result of builtin function abs after validating args. * - * @param mixed $number Should be numeric + * @param mixed $number Should be numeric, or can be an array of numbers * - * @return float|int|string Rounded number + * @return array|float|int|string rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Angle.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Angle.php index 3062481f0e3..cbeec6f4077 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Angle.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Angle.php @@ -2,21 +2,30 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Angle { + use ArrayEnabled; + /** * DEGREES. * * Returns the result of builtin function rad2deg after validating args. * - * @param mixed $number Should be numeric + * @param mixed $number Should be numeric, or can be an array of numbers * - * @return float|string Rounded number + * @return array|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function toDegrees($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -31,12 +40,18 @@ class Angle * * Returns the result of builtin function deg2rad after validating args. * - * @param mixed $number Should be numeric + * @param mixed $number Should be numeric, or can be an array of numbers * - * @return float|string Rounded number + * @return array|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function toRadians($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php index b852eeacf1d..ee4885057f6 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Arabic { + use ArrayEnabled; + private const ROMAN_LOOKUP = [ 'M' => 1000, 'D' => 500, @@ -70,14 +73,20 @@ class Arabic * Excel Function: * ARABIC(text) * - * @param string $roman + * @param mixed $roman Should be a string, or can be an array of strings * - * @return int|string the arabic numberal contrived from the roman numeral + * @return array|int|string the arabic numberal contrived from the roman numeral + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($roman) { + if (is_array($roman)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $roman); + } + // An empty string should return 0 - $roman = substr(trim(strtoupper((string) Functions::flattenSingleValue($roman))), 0, 255); + $roman = substr(trim(strtoupper((string) $roman)), 0, 255); if ($roman === '') { return 0; } @@ -91,7 +100,7 @@ class Arabic try { $arabic = self::calculateArabic(self::strSplit($roman)); } catch (Exception $e) { - return Functions::VALUE(); // Invalid character detected + return ExcelError::VALUE(); // Invalid character detected } if ($negativeNumber) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Base.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Base.php index 4be7b7c7986..2fec9473268 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Base.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Base.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Base { + use ArrayEnabled; + /** * BASE. * @@ -16,24 +19,40 @@ class Base * BASE(Number, Radix [Min_length]) * * @param mixed $number expect float + * Or can be an array of values * @param mixed $radix expect float + * Or can be an array of values * @param mixed $minLength expect int or null + * Or can be an array of values * - * @return string the text representation with the given radix (base) + * @return array|string the text representation with the given radix (base) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($number, $radix, $minLength = null) { + if (is_array($number) || is_array($radix) || is_array($minLength)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $radix, $minLength); + } + try { $number = (float) floor(Helpers::validateNumericNullBool($number)); $radix = (int) Helpers::validateNumericNullBool($radix); } catch (Exception $e) { return $e->getMessage(); } - $minLength = Functions::flattenSingleValue($minLength); + return self::calculate($number, $radix, $minLength); + } + + /** + * @param mixed $minLength + */ + private static function calculate(float $number, int $radix, $minLength): string + { if ($minLength === null || is_numeric($minLength)) { if ($number < 0 || $number >= 2 ** 53 || $radix < 2 || $radix > 36) { - return Functions::NAN(); // Numeric range constraints + return ExcelError::NAN(); // Numeric range constraints } $outcome = strtoupper((string) base_convert("$number", 10, $radix)); @@ -44,6 +63,6 @@ class Base return $outcome; } - return Functions::VALUE(); + return ExcelError::VALUE(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php index 73f54a52f69..635f1bbbb80 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Ceiling { + use ArrayEnabled; + /** * CEILING. * @@ -18,13 +22,21 @@ class Ceiling * Excel Function: * CEILING(number[,significance]) * - * @param float $number the number you want the ceiling - * @param float $significance the multiple to which you want to round + * @param array|float $number the number you want the ceiling + * Or can be an array of values + * @param array|float $significance the multiple to which you want to round + * Or can be an array of values * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function ceiling($number, $significance = null) { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + if ($significance === null) { self::floorCheck1Arg(); } @@ -48,13 +60,22 @@ class Ceiling * CEILING.MATH(number[,significance[,mode]]) * * @param mixed $number Number to round + * Or can be an array of values * @param mixed $significance Significance - * @param int $mode direction to round negative numbers + * Or can be an array of values + * @param array|int $mode direction to round negative numbers + * Or can be an array of values * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function math($number, $significance = null, $mode = 0) { + if (is_array($number) || is_array($significance) || is_array($mode)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance, $mode); + } + try { $number = Helpers::validateNumericNullBool($number); $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); @@ -82,12 +103,20 @@ class Ceiling * CEILING.PRECISE(number[,significance]) * * @param mixed $number the number you want to round - * @param float $significance the multiple to which you want to round + * Or can be an array of values + * @param array|float $significance the multiple to which you want to round + * Or can be an array of values * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function precise($number, $significance = 1) { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + try { $number = Helpers::validateNumericNullBool($number); $significance = Helpers::validateNumericNullSubstitution($significance, null); @@ -125,7 +154,7 @@ class Ceiling return ceil($number / $significance) * $significance; } - return Functions::NAN(); + return ExcelError::NAN(); } private static function floorCheck1Arg(): void diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php index 97508bb1ef0..5a652da0918 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Combinations { + use ArrayEnabled; + /** * COMBIN. * @@ -15,13 +18,19 @@ class Combinations * Excel Function: * COMBIN(numObjs,numInSet) * - * @param mixed $numObjs Number of different objects - * @param mixed $numInSet Number of objects in each combination + * @param mixed $numObjs Number of different objects, or can be an array of numbers + * @param mixed $numInSet Number of objects in each combination, or can be an array of numbers * - * @return float|int|string Number of combinations, or a string containing an error + * @return array|float|int|string Number of combinations, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function withoutRepetition($numObjs, $numInSet) { + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } + try { $numObjs = Helpers::validateNumericNullSubstitution($numObjs, null); $numInSet = Helpers::validateNumericNullSubstitution($numInSet, null); @@ -35,21 +44,27 @@ class Combinations } /** - * COMBIN. + * COMBINA. * * Returns the number of combinations for a given number of items. Use COMBIN to * determine the total possible number of groups for a given number of items. * * Excel Function: - * COMBIN(numObjs,numInSet) + * COMBINA(numObjs,numInSet) * - * @param mixed $numObjs Number of different objects - * @param mixed $numInSet Number of objects in each combination + * @param mixed $numObjs Number of different objects, or can be an array of numbers + * @param mixed $numInSet Number of objects in each combination, or can be an array of numbers * - * @return float|int|string Number of combinations, or a string containing an error + * @return array|float|int|string Number of combinations, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function withRepetition($numObjs, $numInSet) { + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } + try { $numObjs = Helpers::validateNumericNullSubstitution($numObjs, null); $numInSet = Helpers::validateNumericNullSubstitution($numInSet, null); @@ -69,6 +84,8 @@ class Combinations return $e->getMessage(); } - return round(Factorial::fact($numObjs + $numInSet - 1) / Factorial::fact($numObjs - 1)) / Factorial::fact($numInSet); + return round( + Factorial::fact($numObjs + $numInSet - 1) / Factorial::fact($numObjs - 1) + ) / Factorial::fact($numInSet); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php index ce930a83ab3..f65c2c18bf2 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php @@ -2,21 +2,30 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Exp { + use ArrayEnabled; + /** * EXP. * * Returns the result of builtin function exp after validating args. * - * @param mixed $number Should be numeric + * @param mixed $number Should be numeric, or can be an array of numbers * - * @return float|string Rounded number + * @return array|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php index f443f8e59ca..b6883e29074 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; class Factorial { + use ArrayEnabled; + /** * FACT. * @@ -17,12 +20,18 @@ class Factorial * Excel Function: * FACT(factVal) * - * @param float $factVal Factorial Value + * @param array|float $factVal Factorial Value, or can be an array of numbers * - * @return float|int|string Factorial, or a string containing an error + * @return array|float|int|string Factorial, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function fact($factVal) { + if (is_array($factVal)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $factVal); + } + try { $factVal = Helpers::validateNumericNullBool($factVal); Helpers::validateNotNegative($factVal); @@ -53,12 +62,18 @@ class Factorial * Excel Function: * FACTDOUBLE(factVal) * - * @param float $factVal Factorial Value + * @param array|float $factVal Factorial Value, or can be an array of numbers * - * @return float|int|string Double Factorial, or a string containing an error + * @return array|float|int|string Double Factorial, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function factDouble($factVal) { + if (is_array($factVal)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $factVal); + } + try { $factVal = Helpers::validateNumericNullSubstitution($factVal, 0); Helpers::validateNotNegative($factVal); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php index 04e122058d5..2199dda0c1a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Floor { + use ArrayEnabled; + private static function floorCheck1Arg(): void { $compatibility = Functions::getCompatibilityMode(); @@ -24,12 +28,20 @@ class Floor * FLOOR(number[,significance]) * * @param mixed $number Expect float. Number to round + * Or can be an array of values * @param mixed $significance Expect float. Significance + * Or can be an array of values * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function floor($number, $significance = null) { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + if ($significance === null) { self::floorCheck1Arg(); } @@ -53,13 +65,22 @@ class Floor * FLOOR.MATH(number[,significance[,mode]]) * * @param mixed $number Number to round + * Or can be an array of values * @param mixed $significance Significance + * Or can be an array of values * @param mixed $mode direction to round negative numbers + * Or can be an array of values * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function math($number, $significance = null, $mode = 0) { + if (is_array($number) || is_array($significance) || is_array($mode)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance, $mode); + } + try { $number = Helpers::validateNumericNullBool($number); $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); @@ -79,13 +100,21 @@ class Floor * Excel Function: * FLOOR.PRECISE(number[,significance]) * - * @param float $number Number to round - * @param float $significance Significance + * @param array|float $number Number to round + * Or can be an array of values + * @param array|float $significance Significance + * Or can be an array of values * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function precise($number, $significance = 1) { + if (is_array($number) || is_array($significance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $significance); + } + try { $number = Helpers::validateNumericNullBool($number); $significance = Helpers::validateNumericNullSubstitution($significance, null); @@ -104,7 +133,7 @@ class Floor private static function argumentsOkPrecise(float $number, float $significance) { if ($significance == 0.0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } if ($number == 0.0) { return 0.0; @@ -121,7 +150,7 @@ class Floor private static function argsOk(float $number, float $significance, int $mode) { if (!$significance) { - return Functions::DIV0(); + return ExcelError::DIV0(); } if (!$number) { return 0.0; @@ -149,7 +178,7 @@ class Floor private static function argumentsOk(float $number, float $significance) { if ($significance == 0.0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } if ($number == 0.0) { return 0.0; @@ -161,6 +190,6 @@ class Floor return floor($number / $significance) * $significance; } - return Functions::NAN(); + return ExcelError::NAN(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php index 1dd52faa752..f70359988b8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Gcd { @@ -57,7 +58,7 @@ class Gcd } if (count($arrayArgs) <= 0) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $gcd = (int) array_pop($arrayArgs); do { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php index b89644a971b..f34f159b8f8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Helpers { @@ -14,7 +15,7 @@ class Helpers */ public static function verySmallDenominator(float $numerator, float $denominator) { - return (abs($denominator) < 1.0E-12) ? Functions::DIV0() : ($numerator / $denominator); + return (abs($denominator) < 1.0E-12) ? ExcelError::DIV0() : ($numerator / $denominator); } /** @@ -37,7 +38,7 @@ class Helpers return 0 + $number; } - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::throwError($number)); } /** @@ -58,7 +59,7 @@ class Helpers return 0 + $number; } - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::throwError($number)); } /** @@ -72,7 +73,7 @@ class Helpers return; } - throw new Exception($except ?? Functions::NAN()); + throw new Exception($except ?? ExcelError::NAN()); } /** @@ -86,7 +87,7 @@ class Helpers return; } - throw new Exception($except ?? Functions::NAN()); + throw new Exception($except ?? ExcelError::NAN()); } /** @@ -100,7 +101,7 @@ class Helpers return; } - throw new Exception(Functions::DIV0()); + throw new Exception(ExcelError::DIV0()); } public static function returnSign(float $number): int @@ -124,6 +125,6 @@ class Helpers */ public static function numberOrNan($result) { - return is_nan($result) ? Functions::NAN() : $result; + return is_nan($result) ? ExcelError::NAN() : $result; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php index 7aa3d06ae27..f7f7764bc6b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class IntClass { + use ArrayEnabled; + /** * INT. * @@ -14,12 +17,18 @@ class IntClass * Excel Function: * INT(number) * - * @param float $number Number to cast to an integer + * @param array|float $number Number to cast to an integer, or can be an array of numbers * - * @return int|string Integer value, or a string containing an error + * @return array|string Integer value, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php index 46e9816dc68..3b23c1da174 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Lcm { @@ -104,7 +105,7 @@ class Lcm private static function testNonNulls(int $anyNonNulls): void { if (!$anyNonNulls) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php index d6878d88c91..7b07f09d516 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Logarithms { + use ArrayEnabled; + /** * LOG_BASE. * @@ -15,12 +18,20 @@ class Logarithms * LOG(number[,base]) * * @param mixed $number The positive real number for which you want the logarithm + * Or can be an array of values * @param mixed $base The base of the logarithm. If base is omitted, it is assumed to be 10. + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function withBase($number, $base = 10) { + if (is_array($number) || is_array($base)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $base); + } + try { $number = Helpers::validateNumericNullBool($number); Helpers::validatePositive($number); @@ -39,11 +50,18 @@ class Logarithms * Returns the result of builtin function log after validating args. * * @param mixed $number Should be numeric + * Or can be an array of values * - * @return float|string Rounded number + * @return array|float|string Rounded number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function base10($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); Helpers::validatePositive($number); @@ -60,11 +78,18 @@ class Logarithms * Returns the result of builtin function log after validating args. * * @param mixed $number Should be numeric + * Or can be an array of values * - * @return float|string Rounded number + * @return array|float|string Rounded number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function natural($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); Helpers::validatePositive($number); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php index 92e1ff8e7bb..5a5125a8c07 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php @@ -7,12 +7,12 @@ use Matrix\Div0Exception as MatrixDiv0Exception; use Matrix\Exception as MatrixException; use Matrix\Matrix; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class MatrixFunctions { /** - * Convert parameter to matrix. + * Convert parameter to Matrix. * * @param mixed $matrixValues A matrix of values */ @@ -31,7 +31,7 @@ class MatrixFunctions $column = 0; foreach ($matrixRow as $matrixCell) { if ((is_string($matrixCell)) || ($matrixCell === null)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } $matrixData[$row][$column] = $matrixCell; ++$column; @@ -42,6 +42,47 @@ class MatrixFunctions return new Matrix($matrixData); } + /** + * SEQUENCE. + * + * Generates a list of sequential numbers in an array. + * + * Excel Function: + * SEQUENCE(rows,[columns],[start],[step]) + * + * @param mixed $rows the number of rows to return, defaults to 1 + * @param mixed $columns the number of columns to return, defaults to 1 + * @param mixed $start the first number in the sequence, defaults to 1 + * @param mixed $step the amount to increment each subsequent value in the array, defaults to 1 + * + * @return array|string The resulting array, or a string containing an error + */ + public static function sequence($rows = 1, $columns = 1, $start = 1, $step = 1) + { + try { + $rows = (int) Helpers::validateNumericNullSubstitution($rows, 1); + Helpers::validatePositive($rows); + $columns = (int) Helpers::validateNumericNullSubstitution($columns, 1); + Helpers::validatePositive($columns); + $start = Helpers::validateNumericNullSubstitution($start, 1); + $step = Helpers::validateNumericNullSubstitution($step, 1); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($step === 0) { + return array_chunk( + array_fill(0, $rows * $columns, $start), + max($columns, 1) + ); + } + + return array_chunk( + range($start, $start + (($rows * $columns - 1) * $step), $step), + max($columns, 1) + ); + } + /** * MDETERM. * @@ -61,7 +102,7 @@ class MatrixFunctions return $matrix->determinant(); } catch (MatrixException $ex) { - return Functions::VALUE(); + return ExcelError::VALUE(); } catch (Exception $e) { return $e->getMessage(); } @@ -86,9 +127,9 @@ class MatrixFunctions return $matrix->inverse()->toArray(); } catch (MatrixDiv0Exception $e) { - return Functions::NAN(); + return ExcelError::NAN(); } catch (MatrixException $e) { - return Functions::VALUE(); + return ExcelError::VALUE(); } catch (Exception $e) { return $e->getMessage(); } @@ -110,7 +151,7 @@ class MatrixFunctions return $matrixA->multiply($matrixB)->toArray(); } catch (MatrixException $ex) { - return Functions::VALUE(); + return ExcelError::VALUE(); } catch (Exception $e) { return $e->getMessage(); } @@ -127,7 +168,7 @@ class MatrixFunctions { try { $dimension = (int) Helpers::validateNumericNullBool($dimension); - Helpers::validatePositive($dimension, Functions::VALUE()); + Helpers::validatePositive($dimension, ExcelError::VALUE()); $matrix = Builder::createIdentityMatrix($dimension, 0)->toArray(); return $matrix; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php index 595c7fdccec..06258451050 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Operations.php @@ -2,21 +2,33 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Operations { + use ArrayEnabled; + /** * MOD. * * @param mixed $dividend Dividend + * Or can be an array of values * @param mixed $divisor Divisor + * Or can be an array of values * - * @return float|int|string Remainder, or a string containing an error + * @return array|float|int|string Remainder, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function mod($dividend, $divisor) { + if (is_array($dividend) || is_array($divisor)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $dividend, $divisor); + } + try { $dividend = Helpers::validateNumericNullBool($dividend); $divisor = Helpers::validateNumericNullBool($divisor); @@ -40,13 +52,21 @@ class Operations * * Computes x raised to the power y. * - * @param float|int $x - * @param float|int $y + * @param array|float|int $x + * Or can be an array of values + * @param array|float|int $y + * Or can be an array of values * - * @return float|int|string The result, or a string containing an error + * @return array|float|int|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function power($x, $y) { + if (is_array($x) || is_array($y)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $x, $y); + } + try { $x = Helpers::validateNumericNullBool($x); $y = Helpers::validateNumericNullBool($y); @@ -56,10 +76,10 @@ class Operations // Validate parameters if (!$x && !$y) { - return Functions::NAN(); + return ExcelError::NAN(); } if (!$x && $y < 0.0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } // Return @@ -82,29 +102,27 @@ class Operations */ public static function product(...$args) { + $args = array_filter( + Functions::flattenArray($args), + function ($value) { + return $value !== null; + } + ); + // Return value - $returnValue = null; + $returnValue = (count($args) === 0) ? 0.0 : 1.0; // Loop through arguments - foreach (Functions::flattenArray($args) as $arg) { + foreach ($args as $arg) { // Is it a numeric value? if (is_numeric($arg)) { - if ($returnValue === null) { - $returnValue = $arg; - } else { - $returnValue *= $arg; - } + $returnValue *= $arg; } else { - return Functions::VALUE(); + return ExcelError::throwError($arg); } } - // Return - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return (float) $returnValue; } /** @@ -117,12 +135,20 @@ class Operations * QUOTIENT(value1,value2) * * @param mixed $numerator Expect float|int + * Or can be an array of values * @param mixed $denominator Expect float|int + * Or can be an array of values * - * @return int|string + * @return array|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function quotient($numerator, $denominator) { + if (is_array($numerator) || is_array($denominator)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numerator, $denominator); + } + try { $numerator = Helpers::validateNumericNullSubstitution($numerator, 0); $denominator = Helpers::validateNumericNullSubstitution($denominator, 0); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Random.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Random.php index 963a789ac52..22cad2cfd98 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Random.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Random.php @@ -2,10 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Random { + use ArrayEnabled; + /** * RAND. * @@ -13,19 +17,27 @@ class Random */ public static function rand() { - return (mt_rand(0, 10000000)) / 10000000; + return mt_rand(0, 10000000) / 10000000; } /** * RANDBETWEEN. * * @param mixed $min Minimal value + * Or can be an array of values * @param mixed $max Maximal value + * Or can be an array of values * - * @return float|int|string Random number + * @return array|float|int|string Random number + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function randBetween($min, $max) { + if (is_array($min) || is_array($max)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $min, $max); + } + try { $min = (int) Helpers::validateNumericNullBool($min); $max = (int) Helpers::validateNumericNullBool($max); @@ -36,4 +48,52 @@ class Random return mt_rand($min, $max); } + + /** + * RANDARRAY. + * + * Generates a list of sequential numbers in an array. + * + * Excel Function: + * RANDARRAY([rows],[columns],[start],[step]) + * + * @param mixed $rows the number of rows to return, defaults to 1 + * @param mixed $columns the number of columns to return, defaults to 1 + * @param mixed $min the minimum number to be returned, defaults to 0 + * @param mixed $max the maximum number to be returned, defaults to 1 + * @param bool $wholeNumber the type of numbers to return: + * False - Decimal numbers to 15 decimal places. (default) + * True - Whole (integer) numbers + * + * @return array|string The resulting array, or a string containing an error + */ + public static function randArray($rows = 1, $columns = 1, $min = 0, $max = 1, $wholeNumber = false) + { + try { + $rows = (int) Helpers::validateNumericNullSubstitution($rows, 1); + Helpers::validatePositive($rows); + $columns = (int) Helpers::validateNumericNullSubstitution($columns, 1); + Helpers::validatePositive($columns); + $min = Helpers::validateNumericNullSubstitution($min, 1); + $max = Helpers::validateNumericNullSubstitution($max, 1); + + if ($max <= $min) { + return ExcelError::VALUE(); + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return array_chunk( + array_map( + function () use ($min, $max, $wholeNumber) { + return $wholeNumber + ? mt_rand((int) $min, (int) $max) + : (mt_rand() / mt_getrandmax()) * ($max - $min) + $min; + }, + array_fill(0, $rows * $columns, $min) + ), + max($columns, 1) + ); + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php index 71a6df3ad79..05415481c31 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Roman { + use ArrayEnabled; + private const VALUES = [ 45 => ['VL'], 46 => ['VLI'], @@ -800,12 +803,12 @@ class Roman private static function styleOk(int $aValue, int $style): string { - return ($aValue < 0 || $aValue > self::MAX_ROMAN_VALUE) ? Functions::VALUE() : self::valueOk($aValue, $style); + return ($aValue < 0 || $aValue > self::MAX_ROMAN_VALUE) ? ExcelError::VALUE() : self::valueOk($aValue, $style); } public static function calculateRoman(int $aValue, int $style): string { - return ($style < 0 || $style > self::MAX_ROMAN_STYLE) ? Functions::VALUE() : self::styleOk($aValue, $style); + return ($style < 0 || $style > self::MAX_ROMAN_STYLE) ? ExcelError::VALUE() : self::styleOk($aValue, $style); } /** @@ -814,12 +817,20 @@ class Roman * Converts a number to Roman numeral * * @param mixed $aValue Number to convert + * Or can be an array of numbers * @param mixed $style Number indicating one of five possible forms + * Or can be an array of styles * - * @return string Roman numeral, or a string containing an error + * @return array|string Roman numeral, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($aValue, $style = 0) { + if (is_array($aValue) || is_array($style)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $aValue, $style); + } + try { $aValue = Helpers::validateNumericNullBool($aValue); if (is_bool($style)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php index 2ddde90008e..776f7eb9029 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Round.php @@ -2,23 +2,32 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Round { + use ArrayEnabled; + /** * ROUND. * * Returns the result of builtin function round after validating args. * - * @param mixed $number Should be numeric - * @param mixed $precision Should be int + * @param mixed $number Should be numeric, or can be an array of numbers + * @param mixed $precision Should be int, or can be an array of numbers * - * @return float|string Rounded number + * @return array|float|string Rounded number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function round($number, $precision) { + if (is_array($number) || is_array($precision)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $precision); + } + try { $number = Helpers::validateNumericNullBool($number); $precision = Helpers::validateNumericNullBool($precision); @@ -34,13 +43,19 @@ class Round * * Rounds a number up to a specified number of decimal places * - * @param float $number Number to round - * @param int $digits Number of digits to which you want to round $number + * @param array|float $number Number to round, or can be an array of numbers + * @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function up($number, $digits) { + if (is_array($number) || is_array($digits)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $digits); + } + try { $number = Helpers::validateNumericNullBool($number); $digits = (int) Helpers::validateNumericNullSubstitution($digits, null); @@ -64,13 +79,19 @@ class Round * * Rounds a number down to a specified number of decimal places * - * @param float $number Number to round - * @param int $digits Number of digits to which you want to round $number + * @param array|float $number Number to round, or can be an array of numbers + * @param array|int $digits Number of digits to which you want to round $number, or can be an array of numbers * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function down($number, $digits) { + if (is_array($number) || is_array($digits)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $digits); + } + try { $number = Helpers::validateNumericNullBool($number); $digits = (int) Helpers::validateNumericNullSubstitution($digits, null); @@ -94,13 +115,19 @@ class Round * * Rounds a number to the nearest multiple of a specified value * - * @param mixed $number Expect float. Number to round. - * @param mixed $multiple Expect int. Multiple to which you want to round. + * @param mixed $number Expect float. Number to round, or can be an array of numbers + * @param mixed $multiple Expect int. Multiple to which you want to round, or can be an array of numbers. * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function multiple($number, $multiple) { + if (is_array($number) || is_array($multiple)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $number, $multiple); + } + try { $number = Helpers::validateNumericNullSubstitution($number, 0); $multiple = Helpers::validateNumericNullSubstitution($multiple, null); @@ -117,7 +144,7 @@ class Round return round($number * $multiplier) / $multiplier; } - return Functions::NAN(); + return ExcelError::NAN(); } /** @@ -132,12 +159,18 @@ class Round * Excel Function: * EVEN(number) * - * @param float $number Number to round + * @param array|float $number Number to round, or can be an array of numbers * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function even($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -152,12 +185,18 @@ class Round * * Returns number rounded up to the nearest odd integer. * - * @param float $number Number to round + * @param array|float $number Number to round, or can be an array of numbers * - * @return float|string Rounded Number, or a string containing an error + * @return array|float|string Rounded Number, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function odd($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php index 2ada9df4ea9..ecce359fc6e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class SeriesSum { + use ArrayEnabled; + /** * SERIESSUM. * @@ -17,10 +20,14 @@ class SeriesSum * @param mixed $m Step * @param mixed[] $args An array of coefficients for the Data Series * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function evaluate($x, $n, $m, ...$args) { + if (is_array($x) || is_array($n) || is_array($m)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 3, $x, $n, $m, ...$args); + } + try { $x = Helpers::validateNumericNullSubstitution($x, 0); $n = Helpers::validateNumericNullSubstitution($n, 0); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php index a48cf0f9bb6..e40e1f6d367 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php @@ -2,22 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Sign { + use ArrayEnabled; + /** * SIGN. * * Determines the sign of a number. Returns 1 if the number is positive, zero (0) * if the number is 0, and -1 if the number is negative. * - * @param float $number Number to round + * @param array|float $number Number to round, or can be an array of numbers * - * @return int|string sign value, or a string containing an error + * @return array|int|string sign value, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php index 8ead578e325..bb9f15fda5a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php @@ -2,21 +2,30 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Sqrt { + use ArrayEnabled; + /** * SQRT. * * Returns the result of builtin function sqrt after validating args. * - * @param mixed $number Should be numeric + * @param mixed $number Should be numeric, or can be an array of numbers * - * @return float|string square roor + * @return array|float|string square root + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function sqrt($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -31,12 +40,18 @@ class Sqrt * * Returns the square root of (number * pi). * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string Square Root of Number * Pi, or a string containing an error + * @return array|float|string Square Root of Number * Pi, or a string containing an error + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function pi($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullSubstitution($number, 0); Helpers::validateNotNegative($number); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php index 2edb86f7e76..6d8f4723c18 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; class Subtotal @@ -17,7 +18,11 @@ class Subtotal return array_filter( $args, function ($index) use ($cellReference) { - [, $row, ] = explode('.', $index); + $explodeArray = explode('.', $index); + $row = $explodeArray[1] ?? ''; + if (!is_numeric($row)) { + return true; + } return $cellReference->getWorksheet()->getRowDimension($row)->getVisible(); }, @@ -34,12 +39,17 @@ class Subtotal return array_filter( $args, function ($index) use ($cellReference) { - [, $row, $column] = explode('.', $index); + $explodeArray = explode('.', $index); + $row = $explodeArray[1] ?? ''; + $column = $explodeArray[2] ?? ''; $retVal = true; if ($cellReference->getWorksheet()->cellExists($column . $row)) { //take this cell out if it contains the SUBTOTAL or AGGREGATE functions in a formula $isFormula = $cellReference->getWorksheet()->getCell($column . $row)->isFormula(); - $cellFormula = !preg_match('/^=.*\b(SUBTOTAL|AGGREGATE)\s*\(/i', $cellReference->getWorksheet()->getCell($column . $row)->getValue()); + $cellFormula = !preg_match( + '/^=.*\b(SUBTOTAL|AGGREGATE)\s*\(/i', + $cellReference->getWorksheet()->getCell($column . $row)->getValue() ?? '' + ); $retVal = !$isFormula || $cellFormula; } @@ -50,19 +60,18 @@ class Subtotal ); } - /** @var callable[] */ private const CALL_FUNCTIONS = [ - 1 => [Statistical\Averages::class, 'average'], - [Statistical\Counts::class, 'COUNT'], // 2 - [Statistical\Counts::class, 'COUNTA'], // 3 - [Statistical\Maximum::class, 'max'], // 4 - [Statistical\Minimum::class, 'min'], // 5 - [Operations::class, 'product'], // 6 - [Statistical\StandardDeviations::class, 'STDEV'], // 7 - [Statistical\StandardDeviations::class, 'STDEVP'], // 8 - [Sum::class, 'sumIgnoringStrings'], // 9 - [Statistical\Variances::class, 'VAR'], // 10 - [Statistical\Variances::class, 'VARP'], // 11 + 1 => [Statistical\Averages::class, 'average'], // 1 and 101 + [Statistical\Counts::class, 'COUNT'], // 2 and 102 + [Statistical\Counts::class, 'COUNTA'], // 3 and 103 + [Statistical\Maximum::class, 'max'], // 4 and 104 + [Statistical\Minimum::class, 'min'], // 5 and 105 + [Operations::class, 'product'], // 6 and 106 + [Statistical\StandardDeviations::class, 'STDEV'], // 7 and 107 + [Statistical\StandardDeviations::class, 'STDEVP'], // 8 and 108 + [Sum::class, 'sumIgnoringStrings'], // 9 and 109 + [Statistical\Variances::class, 'VAR'], // 10 and 110 + [Statistical\Variances::class, 'VARP'], // 111 and 111 ]; /** @@ -84,7 +93,22 @@ class Subtotal public static function evaluate($functionType, ...$args) { $cellReference = array_pop($args); - $aArgs = Functions::flattenArrayIndexed($args); + $bArgs = Functions::flattenArrayIndexed($args); + $aArgs = []; + // int keys must come before string keys for PHP 8.0+ + // Otherwise, PHP thinks positional args follow keyword + // in the subsequent call to call_user_func_array. + // Fortunately, order of args is unimportant to Subtotal. + foreach ($bArgs as $key => $value) { + if (is_int($key)) { + $aArgs[$key] = $value; + } + } + foreach ($bArgs as $key => $value) { + if (!is_int($key)) { + $aArgs[$key] = $value; + } + } try { $subtotal = (int) Helpers::validateNumericNullBool($functionType); @@ -106,6 +130,6 @@ class Subtotal return call_user_func_array($call, $aArgs); } - return Functions::VALUE(); + return ExcelError::VALUE(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php index 741734d90a7..1a797c8a248 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php @@ -3,6 +3,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Calculation\Information\Value; class Sum { @@ -27,7 +30,7 @@ class Sum // Is it a numeric value? if (is_numeric($arg)) { $returnValue += $arg; - } elseif (Functions::isError($arg)) { + } elseif (ErrorValue::isError($arg)) { return $arg; } } @@ -61,11 +64,11 @@ class Sum $returnValue += $arg; } elseif (is_bool($arg)) { $returnValue += (int) $arg; - } elseif (Functions::isError($arg)) { + } elseif (ErrorValue::isError($arg)) { return $arg; // ignore non-numerics from cell, but fail as literals (except null) } elseif ($arg !== null && !Functions::isCellValue($k)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } } @@ -99,7 +102,7 @@ class Sum $array2 = Functions::flattenArray($matrixData); $count = count($array2); if ($wrkCellCount != $count) { - return Functions::VALUE(); + return ExcelError::VALUE(); } foreach ($array2 as $i => $val) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php index 49fa6381123..34b397cde27 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class SumSquares { @@ -40,7 +41,7 @@ class SumSquares { $count = count($array1); if ($count !== count($array2)) { - throw new Exception(Functions::NA()); + throw new Exception(ExcelError::NA()); } return $count; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosecant.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosecant.php index 3038e6cc0e5..845b6c14654 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosecant.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosecant.php @@ -2,22 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; class Cosecant { + use ArrayEnabled; + /** * CSC. * * Returns the cosecant of an angle. * - * @param float $angle Number + * @param array|float $angle Number, or can be an array of numbers * - * @return float|string The cosecant of the angle + * @return array|float|string The cosecant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function csc($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -32,12 +41,18 @@ class Cosecant * * Returns the hyperbolic cosecant of an angle. * - * @param float $angle Number + * @param array|float $angle Number, or can be an array of numbers * - * @return float|string The hyperbolic cosecant of the angle + * @return array|float|string The hyperbolic cosecant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function csch($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosine.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosine.php index 6c69e126d73..c06f04dfc1b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosine.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cosine.php @@ -2,22 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; class Cosine { + use ArrayEnabled; + /** * COS. * * Returns the result of builtin function cos after validating args. * - * @param mixed $number Should be numeric + * @param mixed $number Should be numeric, or can be an array of numbers * - * @return float|string cosine + * @return array|float|string cosine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function cos($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -32,12 +41,18 @@ class Cosine * * Returns the result of builtin function cosh after validating args. * - * @param mixed $number Should be numeric + * @param mixed $number Should be numeric, or can be an array of numbers * - * @return float|string hyperbolic cosine + * @return array|float|string hyperbolic cosine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function cosh($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -52,12 +67,18 @@ class Cosine * * Returns the arccosine of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The arccosine of the number + * @return array|float|string The arccosine of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function acos($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -72,12 +93,18 @@ class Cosine * * Returns the arc inverse hyperbolic cosine of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The inverse hyperbolic cosine of the number, or an error string + * @return array|float|string The inverse hyperbolic cosine of the number, or an error string + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function acosh($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cotangent.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cotangent.php index 1b796f50f65..eeedef9bfce 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cotangent.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Cotangent.php @@ -2,22 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; class Cotangent { + use ArrayEnabled; + /** * COT. * * Returns the cotangent of an angle. * - * @param float $angle Number + * @param array|float $angle Number, or can be an array of numbers * - * @return float|string The cotangent of the angle + * @return array|float|string The cotangent of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function cot($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -32,12 +41,18 @@ class Cotangent * * Returns the hyperbolic cotangent of an angle. * - * @param float $angle Number + * @param array|float $angle Number, or can be an array of numbers * - * @return float|string The hyperbolic cotangent of the angle + * @return array|float|string The hyperbolic cotangent of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function coth($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -52,12 +67,18 @@ class Cotangent * * Returns the arccotangent of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The arccotangent of the number + * @return array|float|string The arccotangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function acot($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -72,12 +93,18 @@ class Cotangent * * Returns the hyperbolic arccotangent of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The hyperbolic arccotangent of the number + * @return array|float|string The hyperbolic arccotangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function acoth($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Secant.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Secant.php index 70299cb7e31..2d26e5ddf35 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Secant.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Secant.php @@ -2,22 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; class Secant { + use ArrayEnabled; + /** * SEC. * * Returns the secant of an angle. * - * @param float $angle Number + * @param array|float $angle Number, or can be an array of numbers * - * @return float|string The secant of the angle + * @return array|float|string The secant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function sec($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -32,12 +41,18 @@ class Secant * * Returns the hyperbolic secant of an angle. * - * @param float $angle Number + * @param array|float $angle Number, or can be an array of numbers * - * @return float|string The hyperbolic secant of the angle + * @return array|float|string The hyperbolic secant of the angle + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function sech($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Sine.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Sine.php index 2c6a8a0718f..6af568ce23e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Sine.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Sine.php @@ -2,22 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; class Sine { + use ArrayEnabled; + /** * SIN. * * Returns the result of builtin function sin after validating args. * - * @param mixed $angle Should be numeric + * @param mixed $angle Should be numeric, or can be an array of numbers * - * @return float|string sine + * @return array|float|string sine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function sin($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -32,12 +41,18 @@ class Sine * * Returns the result of builtin function sinh after validating args. * - * @param mixed $angle Should be numeric + * @param mixed $angle Should be numeric, or can be an array of numbers * - * @return float|string hyperbolic sine + * @return array|float|string hyperbolic sine + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function sinh($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -52,12 +67,18 @@ class Sine * * Returns the arcsine of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The arcsine of the number + * @return array|float|string The arcsine of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function asin($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -72,12 +93,18 @@ class Sine * * Returns the inverse hyperbolic sine of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The inverse hyperbolic sine of the number + * @return array|float|string The inverse hyperbolic sine of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function asinh($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Tangent.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Tangent.php index 6cd235fb702..9e770210286 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Tangent.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trig/Tangent.php @@ -2,23 +2,32 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Helpers; class Tangent { + use ArrayEnabled; + /** * TAN. * * Returns the result of builtin function tan after validating args. * - * @param mixed $angle Should be numeric + * @param mixed $angle Should be numeric, or can be an array of numbers * - * @return float|string tangent + * @return array|float|string tangent + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function tan($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -33,12 +42,18 @@ class Tangent * * Returns the result of builtin function sinh after validating args. * - * @param mixed $angle Should be numeric + * @param mixed $angle Should be numeric, or can be an array of numbers * - * @return float|string hyperbolic tangent + * @return array|float|string hyperbolic tangent + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function tanh($angle) { + if (is_array($angle)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $angle); + } + try { $angle = Helpers::validateNumericNullBool($angle); } catch (Exception $e) { @@ -53,12 +68,18 @@ class Tangent * * Returns the arctangent of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The arctangent of the number + * @return array|float|string The arctangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function atan($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -73,12 +94,18 @@ class Tangent * * Returns the inverse hyperbolic tangent of a number. * - * @param float $number Number + * @param array|float $number Number, or can be an array of numbers * - * @return float|string The inverse hyperbolic tangent of the number + * @return array|float|string The inverse hyperbolic tangent of the number + * If an array of numbers is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function atanh($number) { + if (is_array($number)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number); + } + try { $number = Helpers::validateNumericNullBool($number); } catch (Exception $e) { @@ -104,13 +131,20 @@ class Tangent * Excel Function: * ATAN2(xCoordinate,yCoordinate) * - * @param mixed $xCoordinate should be float, the x-coordinate of the point - * @param mixed $yCoordinate should be float, the y-coordinate of the point + * @param mixed $xCoordinate should be float, the x-coordinate of the point, or can be an array of numbers + * @param mixed $yCoordinate should be float, the y-coordinate of the point, or can be an array of numbers * - * @return float|string the inverse tangent of the specified x- and y-coordinates, or a string containing an error + * @return array|float|string + * The inverse tangent of the specified x- and y-coordinates, or a string containing an error + * If an array of numbers is passed as one of the arguments, then the returned result will also be an array + * with the same dimensions */ public static function atan2($xCoordinate, $yCoordinate) { + if (is_array($xCoordinate) || is_array($yCoordinate)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $xCoordinate, $yCoordinate); + } + try { $xCoordinate = Helpers::validateNumericNullBool($xCoordinate); $yCoordinate = Helpers::validateNumericNullBool($yCoordinate); @@ -119,7 +153,7 @@ class Tangent } if (($xCoordinate == 0) && ($yCoordinate == 0)) { - return Functions::DIV0(); + return ExcelError::DIV0(); } return atan2($yCoordinate, $xCoordinate); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php index 4b3670f680d..943e209dd24 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php @@ -2,22 +2,33 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; class Trunc { + use ArrayEnabled; + /** * TRUNC. * * Truncates value to the number of fractional digits by number_digits. * - * @param float $value - * @param int $digits + * @param array|float $value + * Or can be an array of values + * @param array|int $digits + * Or can be an array of values * - * @return float|string Truncated value, or a string containing an error + * @return array|float|string Truncated value, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function evaluate($value = 0, $digits = 0) { + if (is_array($value) || is_array($digits)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $digits); + } + try { $value = Helpers::validateNumericNullBool($value); $digits = Helpers::validateNumericNullSubstitution($digits, null); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical.php index d43a85f9045..497c929749a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical.php @@ -130,7 +130,7 @@ class Statistical * @param mixed $rMin * @param mixed $rMax * - * @return float|string + * @return array|float|string */ public static function BETADIST($value, $alpha, $beta, $rMin = 0, $rMax = 1) { @@ -153,7 +153,7 @@ class Statistical * @param float $rMin Minimum value * @param float $rMax Maximum value * - * @return float|string + * @return array|float|string */ public static function BETAINV($probability, $alpha, $beta, $rMin = 0, $rMax = 1) { @@ -179,7 +179,7 @@ class Statistical * @param mixed $probability Probability of success on each trial * @param mixed $cumulative * - * @return float|string + * @return array|float|string */ public static function BINOMDIST($value, $trials, $probability, $cumulative) { @@ -199,7 +199,7 @@ class Statistical * @param float $value Value for the function * @param float $degrees degrees of freedom * - * @return float|string + * @return array|float|string */ public static function CHIDIST($value, $degrees) { @@ -219,7 +219,7 @@ class Statistical * @param float $probability Probability for the function * @param float $degrees degrees of freedom * - * @return float|string + * @return array|float|string */ public static function CHIINV($probability, $degrees) { @@ -240,7 +240,7 @@ class Statistical * @param float $stdDev Standard Deviation * @param float $size * - * @return float|string + * @return array|float|string */ public static function CONFIDENCE($alpha, $stdDev, $size) { @@ -415,7 +415,7 @@ class Statistical * @param float $probability probability of a success on each trial * @param float $alpha criterion value * - * @return int|string + * @return array|int|string */ public static function CRITBINOM($trials, $probability, $alpha) { @@ -460,7 +460,7 @@ class Statistical * @param float $lambda The parameter value * @param bool $cumulative * - * @return float|string + * @return array|float|string */ public static function EXPONDIST($value, $lambda, $cumulative) { @@ -486,7 +486,7 @@ class Statistical * @param bool $cumulative If cumulative is TRUE, F.DIST returns the cumulative distribution function; * if FALSE, it returns the probability density function. * - * @return float|string + * @return array|float|string */ public static function FDIST2($value, $u, $v, $cumulative) { @@ -507,7 +507,7 @@ class Statistical * * @param float $value * - * @return float|string + * @return array|float|string */ public static function FISHER($value) { @@ -528,7 +528,7 @@ class Statistical * * @param float $value * - * @return float|string + * @return array|float|string */ public static function FISHERINV($value) { @@ -549,7 +549,7 @@ class Statistical * @param mixed $yValues array of mixed Data Series Y * @param mixed $xValues of mixed Data Series X * - * @return bool|float|string + * @return array|bool|float|string */ public static function FORECAST($xValue, $yValues, $xValues) { @@ -568,7 +568,7 @@ class Statistical * * @param float $value * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function GAMMAFunction($value) { @@ -590,7 +590,7 @@ class Statistical * @param float $b Parameter to the distribution * @param bool $cumulative * - * @return float|string + * @return array|float|string */ public static function GAMMADIST($value, $a, $b, $cumulative) { @@ -611,7 +611,7 @@ class Statistical * @param float $alpha Parameter to the distribution * @param float $beta Parameter to the distribution * - * @return float|string + * @return array|float|string */ public static function GAMMAINV($probability, $alpha, $beta) { @@ -630,7 +630,7 @@ class Statistical * * @param float $value * - * @return float|string + * @return array|float|string */ public static function GAMMALN($value) { @@ -650,7 +650,7 @@ class Statistical * * @param float $value * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function GAUSS($value) { @@ -742,7 +742,7 @@ class Statistical * @param mixed $populationSuccesses Number of successes in the population * @param mixed $populationNumber Population size * - * @return float|string + * @return array|float|string */ public static function HYPGEOMDIST($sampleSuccesses, $sampleNumber, $populationSuccesses, $populationNumber) { @@ -879,7 +879,7 @@ class Statistical * @param float $mean * @param float $stdDev * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error * * @TODO Try implementing P J Acklam's refinement algorithm for greater * accuracy if I can get my head round the mathematics @@ -905,7 +905,7 @@ class Statistical * @param float $mean * @param float $stdDev * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function LOGNORMDIST($value, $mean, $stdDev) { @@ -928,7 +928,7 @@ class Statistical * @param float $stdDev * @param bool $cumulative * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function LOGNORMDIST2($value, $mean, $stdDev, $cumulative = false) { @@ -1131,7 +1131,7 @@ class Statistical * @param mixed $successes Threshold number of Successes * @param mixed $probability Probability of success on each trial * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function NEGBINOMDIST($failures, $successes, $probability) { @@ -1155,7 +1155,7 @@ class Statistical * @param mixed $stdDev Standard Deviation * @param mixed $cumulative * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function NORMDIST($value, $mean, $stdDev, $cumulative) { @@ -1176,7 +1176,7 @@ class Statistical * @param mixed $mean Mean Value * @param mixed $stdDev Standard Deviation * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function NORMINV($probability, $mean, $stdDev) { @@ -1197,7 +1197,7 @@ class Statistical * * @param mixed $value * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function NORMSDIST($value) { @@ -1219,7 +1219,7 @@ class Statistical * @param mixed $value * @param mixed $cumulative * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function NORMSDIST2($value, $cumulative) { @@ -1238,7 +1238,7 @@ class Statistical * * @param mixed $value * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function NORMSINV($value) { @@ -1308,7 +1308,7 @@ class Statistical * @param int $numObjs Number of different objects * @param int $numInSet Number of objects in each permutation * - * @return float|int|string Number of permutations, or a string containing an error + * @return array|float|int|string Number of permutations, or a string containing an error */ public static function PERMUT($numObjs, $numInSet) { @@ -1331,7 +1331,7 @@ class Statistical * @param mixed $mean Mean Value * @param mixed $cumulative * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function POISSON($value, $mean, $cumulative) { @@ -1480,7 +1480,7 @@ class Statistical * @param float $mean Mean Value * @param float $stdDev Standard Deviation * - * @return float|string Standardized value, or a string containing an error + * @return array|float|string Standardized value, or a string containing an error */ public static function STANDARDIZE($value, $mean, $stdDev) { @@ -1610,7 +1610,7 @@ class Statistical * @param float $degrees degrees of freedom * @param float $tails number of tails (1 or 2) * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function TDIST($value, $degrees, $tails) { @@ -1630,7 +1630,7 @@ class Statistical * @param float $probability Probability for the function * @param float $degrees degrees of freedom * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error */ public static function TINV($probability, $degrees) { @@ -1787,7 +1787,7 @@ class Statistical * @param float $beta Beta Parameter * @param bool $cumulative * - * @return float|string (string if result is an error) + * @return array|float|string (string if result is an error) */ public static function WEIBULL($value, $alpha, $beta, $cumulative) { @@ -1811,7 +1811,7 @@ class Statistical * @param float $m0 Alpha Parameter * @param float $sigma Beta Parameter * - * @return float|string (string if result is an error) + * @return array|float|string (string if result is an error) */ public static function ZTEST($dataSet, $m0, $sigma = null) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php index 1a627e99d6b..85195c88f74 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Averages extends AggregateBase { @@ -24,13 +25,13 @@ class Averages extends AggregateBase $aArgs = Functions::flattenArrayIndexed($args); // Return value - $returnValue = 0; + $returnValue = 0.0; $aMean = self::average(...$args); - if ($aMean === Functions::DIV0()) { - return Functions::NAN(); - } elseif ($aMean === Functions::VALUE()) { - return Functions::VALUE(); + if ($aMean === ExcelError::DIV0()) { + return ExcelError::NAN(); + } elseif ($aMean === ExcelError::VALUE()) { + return ExcelError::VALUE(); } $aCount = 0; @@ -40,7 +41,7 @@ class Averages extends AggregateBase // Strings containing numeric values are only counted if they are string literals (not cell values) // and then only in MS Excel and in Open Office, not in Gnumeric if ((is_string($arg)) && (!is_numeric($arg)) && (!Functions::isCellValue($k))) { - return Functions::VALUE(); + return ExcelError::VALUE(); } if (self::isAcceptedCountable($arg, $k)) { $returnValue += abs($arg - $aMean); @@ -50,7 +51,7 @@ class Averages extends AggregateBase // Return if ($aCount === 0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } return $returnValue / $aCount; @@ -79,7 +80,7 @@ class Averages extends AggregateBase // Strings containing numeric values are only counted if they are string literals (not cell values) // and then only in MS Excel and in Open Office, not in Gnumeric if ((is_string($arg)) && (!is_numeric($arg)) && (!Functions::isCellValue($k))) { - return Functions::VALUE(); + return ExcelError::VALUE(); } if (self::isAcceptedCountable($arg, $k)) { $returnValue += $arg; @@ -92,7 +93,7 @@ class Averages extends AggregateBase return $returnValue / $aCount; } - return Functions::DIV0(); + return ExcelError::DIV0(); } /** @@ -132,7 +133,7 @@ class Averages extends AggregateBase return $returnValue / $aCount; } - return Functions::DIV0(); + return ExcelError::DIV0(); } /** @@ -151,7 +152,7 @@ class Averages extends AggregateBase { $aArgs = Functions::flattenArray($args); - $returnValue = Functions::NAN(); + $returnValue = ExcelError::NAN(); $aArgs = self::filterArguments($aArgs); $valueCount = count($aArgs); @@ -183,7 +184,7 @@ class Averages extends AggregateBase */ public static function mode(...$args) { - $returnValue = Functions::NA(); + $returnValue = ExcelError::NA(); // Loop through arguments $aArgs = Functions::flattenArray($args); @@ -202,7 +203,7 @@ class Averages extends AggregateBase $args, function ($value) { // Is it a numeric value? - return (is_numeric($value)) && (!is_string($value)); + return is_numeric($value) && (!is_string($value)); } ); } @@ -251,7 +252,7 @@ class Averages extends AggregateBase } if ($maxfreq <= 1) { - return Functions::NA(); + return ExcelError::NA(); } return $maxfreqdatum; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php index d4d0c7ee504..001c91eb326 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Averages/Mean.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts; @@ -36,7 +37,7 @@ class Mean } } - return Functions::NAN(); + return ExcelError::NAN(); } /** @@ -57,7 +58,7 @@ class Mean // Loop through arguments $aArgs = Functions::flattenArray($args); if (Minimum::min($aArgs) < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $returnValue = 0; @@ -66,7 +67,7 @@ class Mean // Is it a numeric value? if ((is_numeric($arg)) && (!is_string($arg))) { if ($arg <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $returnValue += (1 / $arg); ++$aCount; @@ -78,7 +79,7 @@ class Mean return 1 / ($returnValue / $aCount); } - return Functions::NA(); + return ExcelError::NA(); } /** @@ -104,7 +105,7 @@ class Mean if ((is_numeric($percent)) && (!is_string($percent))) { if (($percent < 0) || ($percent > 1)) { - return Functions::NAN(); + return ExcelError::NAN(); } $mArgs = []; @@ -126,6 +127,6 @@ class Mean return Averages::average($mArgs); } - return Functions::VALUE(); + return ExcelError::VALUE(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php index c59c3a9ecda..ec2ce34ee17 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php @@ -2,27 +2,36 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Confidence { + use ArrayEnabled; + /** * CONFIDENCE. * * Returns the confidence interval for a population mean * * @param mixed $alpha As a float + * Or can be an array of values * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values * @param mixed $size As an integer + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function CONFIDENCE($alpha, $stdDev, $size) { - $alpha = Functions::flattenSingleValue($alpha); - $stdDev = Functions::flattenSingleValue($stdDev); - $size = Functions::flattenSingleValue($size); + if (is_array($alpha) || is_array($stdDev) || is_array($size)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $alpha, $stdDev, $size); + } try { $alpha = StatisticalValidations::validateFloat($alpha); @@ -33,9 +42,11 @@ class Confidence } if (($alpha <= 0) || ($alpha >= 1) || ($stdDev <= 0) || ($size < 1)) { - return Functions::NAN(); + return ExcelError::NAN(); } + /** @var float */ + $temp = Distributions\StandardNormal::inverse(1 - $alpha / 2); - return Distributions\StandardNormal::inverse(1 - $alpha / 2) * $stdDev / sqrt($size); + return Functions::scalar($temp * $stdDev / sqrt($size)); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Deviations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Deviations.php index 55da9f1ddf5..6b1db3a43b4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Deviations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Deviations.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Deviations { @@ -24,7 +25,7 @@ class Deviations $aMean = Averages::average($aArgs); if (!is_numeric($aMean)) { - return Functions::NAN(); + return ExcelError::NAN(); } // Return value @@ -45,7 +46,7 @@ class Deviations } } - return $aCount === 0 ? Functions::VALUE() : $returnValue; + return $aCount === 0 ? ExcelError::VALUE() : $returnValue; } /** @@ -65,7 +66,7 @@ class Deviations $aArgs = Functions::flattenArrayIndexed($args); $mean = Averages::average($aArgs); if (!is_numeric($mean)) { - return Functions::DIV0(); + return ExcelError::DIV0(); } $stdDev = StandardDeviations::STDEV($aArgs); @@ -90,7 +91,7 @@ class Deviations } } - return Functions::DIV0(); + return ExcelError::DIV0(); } /** @@ -110,11 +111,11 @@ class Deviations $aArgs = Functions::flattenArrayIndexed($args); $mean = Averages::average($aArgs); if (!is_numeric($mean)) { - return Functions::DIV0(); + return ExcelError::DIV0(); } $stdDev = StandardDeviations::STDEV($aArgs); if ($stdDev === 0.0 || is_string($stdDev)) { - return Functions::DIV0(); + return ExcelError::DIV0(); } $count = $summer = 0; @@ -122,7 +123,7 @@ class Deviations foreach ($aArgs as $k => $arg) { if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { } elseif (!is_numeric($arg)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } else { // Is it a numeric value? if ((is_numeric($arg)) && (!is_string($arg))) { @@ -136,6 +137,6 @@ class Deviations return $summer * ($count / (($count - 1) * ($count - 2))); } - return Functions::DIV0(); + return ExcelError::DIV0(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php index 63e6eb4d659..224872bf03d 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Beta { + use ArrayEnabled; + private const MAX_ITERATIONS = 256; private const LOG_GAMMA_X_MAX_VALUE = 2.55e305; @@ -19,20 +23,28 @@ class Beta * Returns the beta distribution. * * @param mixed $value Float value at which you want to evaluate the distribution + * Or can be an array of values * @param mixed $alpha Parameter to the distribution as a float + * Or can be an array of values * @param mixed $beta Parameter to the distribution as a float + * Or can be an array of values * @param mixed $rMin as an float + * Or can be an array of values * @param mixed $rMax as an float + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $alpha, $beta, $rMin = 0.0, $rMax = 1.0) { - $value = Functions::flattenSingleValue($value); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); - $rMin = ($rMin === null) ? 0.0 : Functions::flattenSingleValue($rMin); - $rMax = ($rMax === null) ? 1.0 : Functions::flattenSingleValue($rMax); + if (is_array($value) || is_array($alpha) || is_array($beta) || is_array($rMin) || is_array($rMax)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $alpha, $beta, $rMin, $rMax); + } + + $rMin = $rMin ?? 0.0; + $rMax = $rMax ?? 1.0; try { $value = DistributionValidations::validateFloat($value); @@ -50,7 +62,7 @@ class Beta $rMax = $tmp; } if (($value < $rMin) || ($value > $rMax) || ($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax)) { - return Functions::NAN(); + return ExcelError::NAN(); } $value -= $rMin; @@ -65,20 +77,28 @@ class Beta * Returns the inverse of the Beta distribution. * * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values * @param mixed $alpha Parameter to the distribution as a float + * Or can be an array of values * @param mixed $beta Parameter to the distribution as a float + * Or can be an array of values * @param mixed $rMin Minimum value as a float + * Or can be an array of values * @param mixed $rMax Maximum value as a float + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverse($probability, $alpha, $beta, $rMin = 0.0, $rMax = 1.0) { - $probability = Functions::flattenSingleValue($probability); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); - $rMin = ($rMin === null) ? 0.0 : Functions::flattenSingleValue($rMin); - $rMax = ($rMax === null) ? 1.0 : Functions::flattenSingleValue($rMax); + if (is_array($probability) || is_array($alpha) || is_array($beta) || is_array($rMin) || is_array($rMax)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $alpha, $beta, $rMin, $rMax); + } + + $rMin = $rMin ?? 0.0; + $rMax = $rMax ?? 1.0; try { $probability = DistributionValidations::validateProbability($probability); @@ -96,7 +116,7 @@ class Beta $rMax = $tmp; } if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0.0)) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::calculateInverse($probability, $alpha, $beta, $rMin, $rMax); @@ -124,7 +144,7 @@ class Beta } if ($i === self::MAX_ITERATIONS) { - return Functions::NA(); + return ExcelError::NA(); } return round($rMin + $guess * ($rMax - $rMin), 12); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php index 9631236ab89..02b53e888ee 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php @@ -2,12 +2,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations; class Binomial { + use ArrayEnabled; + /** * BINOMDIST. * @@ -18,17 +22,23 @@ class Binomial * babies born are male. * * @param mixed $value Integer number of successes in trials + * Or can be an array of values * @param mixed $trials Integer umber of trials + * Or can be an array of values * @param mixed $probability Probability of success on each trial as a float + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $trials, $probability, $cumulative) { - $value = Functions::flattenSingleValue($value); - $trials = Functions::flattenSingleValue($trials); - $probability = Functions::flattenSingleValue($probability); + if (is_array($value) || is_array($trials) || is_array($probability) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $trials, $probability, $cumulative); + } try { $value = DistributionValidations::validateInt($value); @@ -40,14 +50,16 @@ class Binomial } if (($value < 0) || ($value > $trials)) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative) { return self::calculateCumulativeBinomial($value, $trials, $probability); } + /** @var float */ + $comb = Combinations::withoutRepetition($trials, $value); - return Combinations::withoutRepetition($trials, $value) * $probability ** $value + return $comb * $probability ** $value * (1 - $probability) ** ($trials - $value); } @@ -58,19 +70,26 @@ class Binomial * of trials falling into a specified range. * * @param mixed $trials Integer number of trials + * Or can be an array of values * @param mixed $probability Probability of success on each trial as a float + * Or can be an array of values * @param mixed $successes The integer number of successes in trials + * Or can be an array of values * @param mixed $limit Upper limit for successes in trials as null, or an integer * If null, then this will indicate the same as the number of Successes + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function range($trials, $probability, $successes, $limit = null) { - $trials = Functions::flattenSingleValue($trials); - $probability = Functions::flattenSingleValue($probability); - $successes = Functions::flattenSingleValue($successes); - $limit = ($limit === null) ? $successes : Functions::flattenSingleValue($limit); + if (is_array($trials) || is_array($probability) || is_array($successes) || is_array($limit)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $successes, $limit); + } + + $limit = $limit ?? $successes; try { $trials = DistributionValidations::validateInt($trials); @@ -82,15 +101,17 @@ class Binomial } if (($successes < 0) || ($successes > $trials)) { - return Functions::NAN(); + return ExcelError::NAN(); } if (($limit < 0) || ($limit > $trials) || $limit < $successes) { - return Functions::NAN(); + return ExcelError::NAN(); } $summer = 0; for ($i = $successes; $i <= $limit; ++$i) { - $summer += Combinations::withoutRepetition($trials, $i) * $probability ** $i + /** @var float */ + $comb = Combinations::withoutRepetition($trials, $i); + $summer += $comb * $probability ** $i * (1 - $probability) ** ($trials - $i); } @@ -107,19 +128,24 @@ class Binomial * variable. Like the binomial, trials are assumed to be independent. * * @param mixed $failures Number of Failures as an integer + * Or can be an array of values * @param mixed $successes Threshold number of Successes as an integer + * Or can be an array of values * @param mixed $probability Probability of success on each trial as a float + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions * * TODO Add support for the cumulative flag not present for NEGBINOMDIST, but introduced for NEGBINOM.DIST * The cumulative default should be false to reflect the behaviour of NEGBINOMDIST */ public static function negative($failures, $successes, $probability) { - $failures = Functions::flattenSingleValue($failures); - $successes = Functions::flattenSingleValue($successes); - $probability = Functions::flattenSingleValue($probability); + if (is_array($failures) || is_array($successes) || is_array($probability)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $failures, $successes, $probability); + } try { $failures = DistributionValidations::validateInt($failures); @@ -130,35 +156,42 @@ class Binomial } if (($failures < 0) || ($successes < 1)) { - return Functions::NAN(); + return ExcelError::NAN(); } if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { if (($failures + $successes - 1) <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } } + /** @var float */ + $comb = Combinations::withoutRepetition($failures + $successes - 1, $successes - 1); - return (Combinations::withoutRepetition($failures + $successes - 1, $successes - 1)) + return $comb * ($probability ** $successes) * ((1 - $probability) ** $failures); } /** - * CRITBINOM. + * BINOM.INV. * * Returns the smallest value for which the cumulative binomial distribution is greater * than or equal to a criterion value * * @param mixed $trials number of Bernoulli trials as an integer + * Or can be an array of values * @param mixed $probability probability of a success on each trial as a float + * Or can be an array of values * @param mixed $alpha criterion value as a float + * Or can be an array of values * - * @return int|string + * @return array|int|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverse($trials, $probability, $alpha) { - $trials = Functions::flattenSingleValue($trials); - $probability = Functions::flattenSingleValue($probability); - $alpha = Functions::flattenSingleValue($alpha); + if (is_array($trials) || is_array($probability) || is_array($alpha)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $alpha); + } try { $trials = DistributionValidations::validateInt($trials); @@ -169,9 +202,9 @@ class Binomial } if ($trials < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } elseif (($alpha < 0.0) || ($alpha > 1.0)) { - return Functions::NAN(); + return ExcelError::NAN(); } $successes = 0; @@ -193,7 +226,9 @@ class Binomial { $summer = 0; for ($i = 0; $i <= $value; ++$i) { - $summer += Combinations::withoutRepetition($trials, $i) * $probability ** $i + /** @var float */ + $comb = Combinations::withoutRepetition($trials, $i); + $summer += $comb * $probability ** $i * (1 - $probability) ** ($trials - $i); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 5165d6397ed..c87433648ef 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class ChiSquared { + use ArrayEnabled; + private const MAX_ITERATIONS = 256; private const EPS = 2.22e-16; @@ -17,14 +21,19 @@ class ChiSquared * Returns the one-tailed probability of the chi-squared distribution. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $degrees Integer degrees of freedom + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distributionRightTail($value, $degrees) { - $value = Functions::flattenSingleValue($value); - $degrees = Functions::flattenSingleValue($degrees); + if (is_array($value) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees); + } try { $value = DistributionValidations::validateFloat($value); @@ -34,14 +43,14 @@ class ChiSquared } if ($degrees < 1) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($value < 0) { if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { return 1; } - return Functions::NAN(); + return ExcelError::NAN(); } return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2)); @@ -53,16 +62,21 @@ class ChiSquared * Returns the one-tailed probability of the chi-squared distribution. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $degrees Integer degrees of freedom + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distributionLeftTail($value, $degrees, $cumulative) { - $value = Functions::flattenSingleValue($value); - $degrees = Functions::flattenSingleValue($degrees); - $cumulative = Functions::flattenSingleValue($cumulative); + if (is_array($value) || is_array($degrees) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -73,21 +87,21 @@ class ChiSquared } if ($degrees < 1) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($value < 0) { if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { return 1; } - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative === true) { return 1 - self::distributionRightTail($value, $degrees); } - return (($value ** (($degrees / 2) - 1) * exp(-$value / 2))) / + return ($value ** (($degrees / 2) - 1) * exp(-$value / 2)) / ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2)); } @@ -97,14 +111,19 @@ class ChiSquared * Returns the inverse of the right-tailed probability of the chi-squared distribution. * * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values * @param mixed $degrees Integer degrees of freedom + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverseRightTail($probability, $degrees) { - $probability = Functions::flattenSingleValue($probability); - $degrees = Functions::flattenSingleValue($degrees); + if (is_array($probability) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees); + } try { $probability = DistributionValidations::validateProbability($probability); @@ -114,7 +133,7 @@ class ChiSquared } if ($degrees < 1) { - return Functions::NAN(); + return ExcelError::NAN(); } $callback = function ($value) use ($degrees) { @@ -133,14 +152,19 @@ class ChiSquared * Returns the inverse of the left-tailed probability of the chi-squared distribution. * * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values * @param mixed $degrees Integer degrees of freedom + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverseLeftTail($probability, $degrees) { - $probability = Functions::flattenSingleValue($probability); - $degrees = Functions::flattenSingleValue($degrees); + if (is_array($probability) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees); + } try { $probability = DistributionValidations::validateProbability($probability); @@ -150,7 +174,7 @@ class ChiSquared } if ($degrees < 1) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::inverseLeftTailCalculation($probability, $degrees); @@ -178,22 +202,22 @@ class ChiSquared $countActuals = count($actual); $countExpected = count($expected); if ($countActuals !== $countExpected || $countActuals === 1) { - return Functions::NAN(); + return ExcelError::NAN(); } $result = 0.0; for ($i = 0; $i < $countActuals; ++$i) { if ($expected[$i] == 0.0) { - return Functions::DIV0(); + return ExcelError::DIV0(); } elseif ($expected[$i] < 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } $result += (($actual[$i] - $expected[$i]) ** 2) / $expected[$i]; } $degrees = self::degrees($rows, $columns); - $result = self::distributionRightTail($result, $degrees); + $result = Functions::scalar(self::distributionRightTail($result, $degrees)); return $result; } @@ -257,6 +281,7 @@ class ChiSquared // Relative error controlled by the eps parameter private static function gser($n, $x) { + /** @var float */ $gln = Gamma::ln($n / 2); $a = 0.5 * $n; $ap = $a; @@ -280,6 +305,7 @@ class ChiSquared // Relative error controlled by the eps parameter private static function gcf($n, $x) { + /** @var float */ $gln = Gamma::ln($n / 2); $a = 0.5 * $n; $b = $x + 1 - $a; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php index 57ef00af04a..bd45a22981d 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/DistributionValidations.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StatisticalValidations; class DistributionValidations extends StatisticalValidations @@ -16,7 +16,7 @@ class DistributionValidations extends StatisticalValidations $probability = self::validateFloat($probability); if ($probability < 0.0 || $probability > 1.0) { - throw new Exception(Functions::NAN()); + throw new Exception(ExcelError::NAN()); } return $probability; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php index b3fd9460c1e..a03671cccde 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Exponential { + use ArrayEnabled; + /** * EXPONDIST. * @@ -15,16 +18,21 @@ class Exponential * use EXPONDIST to determine the probability that the process takes at most 1 minute. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $lambda The parameter value as a float + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $lambda, $cumulative) { - $value = Functions::flattenSingleValue($value); - $lambda = Functions::flattenSingleValue($lambda); - $cumulative = Functions::flattenSingleValue($cumulative); + if (is_array($value) || is_array($lambda) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $lambda, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -35,7 +43,7 @@ class Exponential } if (($value < 0) || ($lambda < 0)) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative === true) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php index 54b1950d6d9..ff413b6b82f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class F { + use ArrayEnabled; + /** * F.DIST. * @@ -16,18 +19,23 @@ class F * if the variability in the females is different from that found in the males. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $u The numerator degrees of freedom as an integer + * Or can be an array of values * @param mixed $v The denominator degrees of freedom as an integer + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $u, $v, $cumulative) { - $value = Functions::flattenSingleValue($value); - $u = Functions::flattenSingleValue($u); - $v = Functions::flattenSingleValue($v); - $cumulative = Functions::flattenSingleValue($cumulative); + if (is_array($value) || is_array($u) || is_array($v) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $u, $v, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -39,7 +47,7 @@ class F } if ($value < 0 || $u < 1 || $v < 1) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php index 923bf02d909..df9906ea9f6 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Fisher { + use ArrayEnabled; + /** * FISHER. * @@ -15,12 +18,17 @@ class Fisher * testing on the correlation coefficient. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value) { - $value = Functions::flattenSingleValue($value); + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } try { DistributionValidations::validateFloat($value); @@ -29,7 +37,7 @@ class Fisher } if (($value <= -1) || ($value >= 1)) { - return Functions::NAN(); + return ExcelError::NAN(); } return 0.5 * log((1 + $value) / (1 - $value)); @@ -43,12 +51,17 @@ class Fisher * FISHERINV(y) = x. * * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverse($probability) { - $probability = Functions::flattenSingleValue($probability); + if (is_array($probability)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $probability); + } try { DistributionValidations::validateFloat($probability); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php index 2c6ed670a8e..216f234c1ed 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php @@ -2,23 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Gamma extends GammaBase { + use ArrayEnabled; + /** * GAMMA. * * Return the gamma function value. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function gamma($value) { - $value = Functions::flattenSingleValue($value); + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } try { $value = DistributionValidations::validateFloat($value); @@ -27,7 +35,7 @@ class Gamma extends GammaBase } if ((((int) $value) == ((float) $value)) && $value <= 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::gammaValue($value); @@ -39,17 +47,23 @@ class Gamma extends GammaBase * Returns the gamma distribution. * * @param mixed $value Float Value at which you want to evaluate the distribution + * Or can be an array of values * @param mixed $a Parameter to the distribution as a float + * Or can be an array of values * @param mixed $b Parameter to the distribution as a float + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $a, $b, $cumulative) { - $value = Functions::flattenSingleValue($value); - $a = Functions::flattenSingleValue($a); - $b = Functions::flattenSingleValue($b); + if (is_array($value) || is_array($a) || is_array($b) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $a, $b, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -61,7 +75,7 @@ class Gamma extends GammaBase } if (($value < 0) || ($a <= 0) || ($b <= 0)) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::calculateDistribution($value, $a, $b, $cumulative); @@ -73,16 +87,21 @@ class Gamma extends GammaBase * Returns the inverse of the Gamma distribution. * * @param mixed $probability Float probability at which you want to evaluate the distribution + * Or can be an array of values * @param mixed $alpha Parameter to the distribution as a float + * Or can be an array of values * @param mixed $beta Parameter to the distribution as a float + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverse($probability, $alpha, $beta) { - $probability = Functions::flattenSingleValue($probability); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); + if (is_array($probability) || is_array($alpha) || is_array($beta)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $alpha, $beta); + } try { $probability = DistributionValidations::validateProbability($probability); @@ -93,7 +112,7 @@ class Gamma extends GammaBase } if (($alpha <= 0.0) || ($beta <= 0.0)) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::calculateInverse($probability, $alpha, $beta); @@ -105,12 +124,17 @@ class Gamma extends GammaBase * Returns the natural logarithm of the gamma function. * * @param mixed $value Float Value at which you want to evaluate the distribution + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function ln($value) { - $value = Functions::flattenSingleValue($value); + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } try { $value = DistributionValidations::validateFloat($value); @@ -119,7 +143,7 @@ class Gamma extends GammaBase } if ($value <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } return log(self::gammaValue($value)); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php index 89170f7cada..404230bafb4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; abstract class GammaBase { @@ -65,7 +66,7 @@ abstract class GammaBase } if ($i === self::MAX_ITERATIONS) { - return Functions::NA(); + return ExcelError::NA(); } return $x; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php index fe30c0879e4..b3ad69d1af2 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations; class HyperGeometric { + use ArrayEnabled; + /** * HYPGEOMDIST. * @@ -15,18 +18,32 @@ class HyperGeometric * sample successes, given the sample size, population successes, and population size. * * @param mixed $sampleSuccesses Integer number of successes in the sample + * Or can be an array of values * @param mixed $sampleNumber Integer size of the sample + * Or can be an array of values * @param mixed $populationSuccesses Integer number of successes in the population + * Or can be an array of values * @param mixed $populationNumber Integer population size + * Or can be an array of values * - * @return float|string + * @return array|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($sampleSuccesses, $sampleNumber, $populationSuccesses, $populationNumber) { - $sampleSuccesses = Functions::flattenSingleValue($sampleSuccesses); - $sampleNumber = Functions::flattenSingleValue($sampleNumber); - $populationSuccesses = Functions::flattenSingleValue($populationSuccesses); - $populationNumber = Functions::flattenSingleValue($populationNumber); + if ( + is_array($sampleSuccesses) || is_array($sampleNumber) || + is_array($populationSuccesses) || is_array($populationNumber) + ) { + return self::evaluateArrayArguments( + [self::class, __FUNCTION__], + $sampleSuccesses, + $sampleNumber, + $populationSuccesses, + $populationNumber + ); + } try { $sampleSuccesses = DistributionValidations::validateInt($sampleSuccesses); @@ -38,13 +55,13 @@ class HyperGeometric } if (($sampleSuccesses < 0) || ($sampleSuccesses > $sampleNumber) || ($sampleSuccesses > $populationSuccesses)) { - return Functions::NAN(); + return ExcelError::NAN(); } if (($sampleNumber <= 0) || ($sampleNumber > $populationNumber)) { - return Functions::NAN(); + return ExcelError::NAN(); } if (($populationSuccesses <= 0) || ($populationSuccesses > $populationNumber)) { - return Functions::NAN(); + return ExcelError::NAN(); } $successesPopulationAndSample = (float) Combinations::withoutRepetition($populationSuccesses, $sampleSuccesses); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php index e15237736ca..d572d234f36 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class LogNormal { + use ArrayEnabled; + /** * LOGNORMDIST. * @@ -14,16 +17,21 @@ class LogNormal * with parameters mean and standard_dev. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $mean Mean value as a float + * Or can be an array of values * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function cumulative($value, $mean, $stdDev) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); + if (is_array($value) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev); + } try { $value = DistributionValidations::validateFloat($value); @@ -34,7 +42,7 @@ class LogNormal } if (($value <= 0) || ($stdDev <= 0)) { - return Functions::NAN(); + return ExcelError::NAN(); } return StandardNormal::cumulative((log($value) - $mean) / $stdDev); @@ -47,18 +55,23 @@ class LogNormal * with parameters mean and standard_dev. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $mean Mean value as a float + * Or can be an array of values * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $mean, $stdDev, $cumulative = false) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); - $cumulative = Functions::flattenSingleValue($cumulative); + if (is_array($value) || is_array($mean) || is_array($stdDev) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -70,7 +83,7 @@ class LogNormal } if (($value <= 0) || ($stdDev <= 0)) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative === true) { @@ -87,10 +100,15 @@ class LogNormal * Returns the inverse of the lognormal cumulative distribution * * @param mixed $probability Float probability for which we want the value + * Or can be an array of values * @param mixed $mean Mean Value as a float + * Or can be an array of values * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions * * @TODO Try implementing P J Acklam's refinement algorithm for greater * accuracy if I can get my head round the mathematics @@ -98,9 +116,9 @@ class LogNormal */ public static function inverse($probability, $mean, $stdDev) { - $probability = Functions::flattenSingleValue($probability); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); + if (is_array($probability) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev); + } try { $probability = DistributionValidations::validateProbability($probability); @@ -111,9 +129,11 @@ class LogNormal } if ($stdDev <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } + /** @var float */ + $inverse = StandardNormal::inverse($probability); - return exp($mean + $stdDev * StandardNormal::inverse($probability)); + return exp($mean + $stdDev * $inverse); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php index 26211672576..b994864e635 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class NewtonRaphson { @@ -54,7 +55,7 @@ class NewtonRaphson } if ($i == self::MAX_ITERATIONS) { - return Functions::NA(); + return ExcelError::NA(); } return $x; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php index 4d158b8c8f7..67533c499ff 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Normal { + use ArrayEnabled; + public const SQRT2PI = 2.5066282746310005024157652848110452530069867406099; /** @@ -18,17 +21,23 @@ class Normal * testing. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $mean Mean value as a float + * Or can be an array of values * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $mean, $stdDev, $cumulative) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); + if (is_array($value) || is_array($mean) || is_array($stdDev) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -40,7 +49,7 @@ class Normal } if ($stdDev < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative) { @@ -56,16 +65,21 @@ class Normal * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. * * @param mixed $probability Float probability for which we want the value + * Or can be an array of values * @param mixed $mean Mean Value as a float + * Or can be an array of values * @param mixed $stdDev Standard Deviation as a float + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverse($probability, $mean, $stdDev) { - $probability = Functions::flattenSingleValue($probability); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); + if (is_array($probability) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $mean, $stdDev); + } try { $probability = DistributionValidations::validateProbability($probability); @@ -76,7 +90,7 @@ class Normal } if ($stdDev < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } return (self::inverseNcdf($probability) * $stdDev) + $mean; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php index e7252e02898..041c34a0fc9 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class Poisson { + use ArrayEnabled; + /** * POISSON. * @@ -16,15 +19,21 @@ class Poisson * cars arriving at a toll plaza in 1 minute. * * @param mixed $value Float value for which we want the probability + * Or can be an array of values * @param mixed $mean Mean value as a float + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $mean, $cumulative) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); + if (is_array($value) || is_array($mean) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -35,19 +44,23 @@ class Poisson } if (($value < 0) || ($mean < 0)) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative) { $summer = 0; $floor = floor($value); for ($i = 0; $i <= $floor; ++$i) { - $summer += $mean ** $i / MathTrig\Factorial::fact($i); + /** @var float */ + $fact = MathTrig\Factorial::fact($i); + $summer += $mean ** $i / $fact; } return exp(0 - $mean) * $summer; } + /** @var float */ + $fact = MathTrig\Factorial::fact($value); - return (exp(0 - $mean) * $mean ** $value) / MathTrig\Factorial::fact($value); + return (exp(0 - $mean) * $mean ** $value) / $fact; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php index d10f02a5f74..a655fa745f8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php @@ -2,12 +2,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; class StandardNormal { + use ArrayEnabled; + /** * NORMSDIST. * @@ -15,9 +19,16 @@ class StandardNormal * a mean of 0 (zero) and a standard deviation of one. Use this function in place of a * table of standard normal curve areas. * - * @param mixed $value Float value for which we want the probability + * NOTE: We don't need to check for arrays to array-enable this function, because that is already + * handled by the logic in Normal::distribution() + * All we need to do is pass the value through as scalar or as array. * - * @return float|string The result, or a string containing an error + * @param mixed $value Float value for which we want the probability + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function cumulative($value) { @@ -31,10 +42,18 @@ class StandardNormal * a mean of 0 (zero) and a standard deviation of one. Use this function in place of a * table of standard normal curve areas. * - * @param mixed $value Float value for which we want the probability - * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * NOTE: We don't need to check for arrays to array-enable this function, because that is already + * handled by the logic in Normal::distribution() + * All we need to do is pass the value and cumulative through as scalar or as array. * - * @return float|string The result, or a string containing an error + * @param mixed $value Float value for which we want the probability + * Or can be an array of values + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $cumulative) { @@ -46,9 +65,16 @@ class StandardNormal * * Returns the inverse of the standard normal cumulative distribution * - * @param mixed $value Float probability for which we want the value + * @param mixed $value float probability for which we want the value + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * NOTE: We don't need to check for arrays to array-enable this function, because that is already + * handled by the logic in Normal::inverse() + * All we need to do is pass the value through as scalar or as array + * + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverse($value) { @@ -62,17 +88,25 @@ class StandardNormal * the mean and z standard deviations from the mean. * * @param mixed $value + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function gauss($value) { - $value = Functions::flattenSingleValue($value); - if (!is_numeric($value)) { - return Functions::VALUE(); + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); } - return self::distribution($value, true) - 0.5; + if (!is_numeric($value)) { + return ExcelError::VALUE(); + } + /** @var float */ + $dist = self::distribution($value, true); + + return $dist - 0.5; } /** @@ -85,22 +119,29 @@ class StandardNormal * * @param mixed $dataSet The dataset should be an array of float values for the observations * @param mixed $m0 Alpha Parameter + * Or can be an array of values * @param mixed $sigma A null or float value for the Beta (Standard Deviation) Parameter; * if null, we use the standard deviation of the dataset + * Or can be an array of values * - * @return float|string (string if result is an error) + * @return array|float|string (string if result is an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function zTest($dataSet, $m0, $sigma = null) { + if (is_array($m0) || is_array($sigma)) { + return self::evaluateArrayArgumentsSubsetFrom([self::class, __FUNCTION__], 1, $dataSet, $m0, $sigma); + } + $dataSet = Functions::flattenArrayIndexed($dataSet); - $m0 = Functions::flattenSingleValue($m0); - $sigma = Functions::flattenSingleValue($sigma); if (!is_numeric($m0) || ($sigma !== null && !is_numeric($sigma))) { - return Functions::VALUE(); + return ExcelError::VALUE(); } if ($sigma === null) { + /** @var float */ $sigma = StandardDeviations::STDEV($dataSet); } $n = count($dataSet); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php index 45d590dd687..8afc5c4b20b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php @@ -2,11 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class StudentT { + use ArrayEnabled; + private const MAX_ITERATIONS = 256; /** @@ -15,16 +19,21 @@ class StudentT * Returns the probability of Student's T distribution. * * @param mixed $value Float value for the distribution + * Or can be an array of values * @param mixed $degrees Integer value for degrees of freedom + * Or can be an array of values * @param mixed $tails Integer value for the number of tails (1 or 2) + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $degrees, $tails) { - $value = Functions::flattenSingleValue($value); - $degrees = Functions::flattenSingleValue($degrees); - $tails = Functions::flattenSingleValue($tails); + if (is_array($value) || is_array($degrees) || is_array($tails)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $degrees, $tails); + } try { $value = DistributionValidations::validateFloat($value); @@ -35,7 +44,7 @@ class StudentT } if (($value < 0) || ($degrees < 1) || ($tails < 1) || ($tails > 2)) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::calculateDistribution($value, $degrees, $tails); @@ -47,14 +56,19 @@ class StudentT * Returns the one-tailed probability of the chi-squared distribution. * * @param mixed $probability Float probability for the function + * Or can be an array of values * @param mixed $degrees Integer value for degrees of freedom + * Or can be an array of values * - * @return float|string The result, or a string containing an error + * @return array|float|string The result, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function inverse($probability, $degrees) { - $probability = Functions::flattenSingleValue($probability); - $degrees = Functions::flattenSingleValue($degrees); + if (is_array($probability) || is_array($degrees)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $probability, $degrees); + } try { $probability = DistributionValidations::validateProbability($probability); @@ -64,7 +78,7 @@ class StudentT } if ($degrees <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $callback = function ($value) use ($degrees) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php index ecec8a85640..51392c41f78 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Weibull { + use ArrayEnabled; + /** * WEIBULL. * @@ -14,18 +17,23 @@ class Weibull * analysis, such as calculating a device's mean time to failure. * * @param mixed $value Float value for the distribution + * Or can be an array of values * @param mixed $alpha Float alpha Parameter + * Or can be an array of values * @param mixed $beta Float beta Parameter + * Or can be an array of values * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) + * Or can be an array of values * - * @return float|string (string if result is an error) + * @return array|float|string (string if result is an error) + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function distribution($value, $alpha, $beta, $cumulative) { - $value = Functions::flattenSingleValue($value); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); - $cumulative = Functions::flattenSingleValue($cumulative); + if (is_array($value) || is_array($alpha) || is_array($beta) || is_array($cumulative)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $alpha, $beta, $cumulative); + } try { $value = DistributionValidations::validateFloat($value); @@ -37,7 +45,7 @@ class Weibull } if (($value < 0) || ($alpha <= 0) || ($beta <= 0)) { - return Functions::NAN(); + return ExcelError::NAN(); } if ($cumulative) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php index 16cb06e86ef..99738232e18 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Percentiles { @@ -37,7 +38,7 @@ class Percentiles } if (($entry < 0) || ($entry > 1)) { - return Functions::NAN(); + return ExcelError::NAN(); } $mArgs = self::percentileFilterValues($aArgs); @@ -56,7 +57,7 @@ class Percentiles return $mArgs[$iBase] + (($mArgs[$iNext] - $mArgs[$iBase]) * $iProportion); } - return Functions::NAN(); + return ExcelError::NAN(); } /** @@ -89,13 +90,13 @@ class Percentiles $valueSet = self::rankFilterValues($valueSet); $valueCount = count($valueSet); if ($valueCount == 0) { - return Functions::NA(); + return ExcelError::NA(); } sort($valueSet, SORT_NUMERIC); $valueAdjustor = $valueCount - 1; if (($value < $valueSet[0]) || ($value > $valueSet[$valueAdjustor])) { - return Functions::NA(); + return ExcelError::NA(); } $pos = array_search($value, $valueSet); @@ -138,7 +139,7 @@ class Percentiles $entry = floor($entry); $entry /= 4; if (($entry < 0) || ($entry > 1)) { - return Functions::NAN(); + return ExcelError::NAN(); } return self::PERCENTILE($aArgs, $entry); @@ -177,7 +178,7 @@ class Percentiles $pos = array_search($value, $valueSet); if ($pos === false) { - return Functions::NA(); + return ExcelError::NA(); } return ++$pos; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php index 272a8a53dbf..5d9d3048371 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -2,13 +2,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Shared\IntOrFloat; class Permutations { + use ArrayEnabled; + /** * PERMUT. * @@ -19,14 +22,19 @@ class Permutations * for lottery-style probability calculations. * * @param mixed $numObjs Integer number of different objects + * Or can be an array of values * @param mixed $numInSet Integer number of objects in each permutation + * Or can be an array of values * - * @return float|int|string Number of permutations, or a string containing an error + * @return array|float|int|string Number of permutations, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function PERMUT($numObjs, $numInSet) { - $numObjs = Functions::flattenSingleValue($numObjs); - $numInSet = Functions::flattenSingleValue($numInSet); + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } try { $numObjs = StatisticalValidations::validateInt($numObjs); @@ -36,7 +44,7 @@ class Permutations } if ($numObjs < $numInSet) { - return Functions::NAN(); + return ExcelError::NAN(); } $result = round(MathTrig\Factorial::fact($numObjs) / MathTrig\Factorial::fact($numObjs - $numInSet)); @@ -50,14 +58,19 @@ class Permutations * that can be selected from the total objects. * * @param mixed $numObjs Integer number of different objects + * Or can be an array of values * @param mixed $numInSet Integer number of objects in each permutation + * Or can be an array of values * - * @return float|int|string Number of permutations, or a string containing an error + * @return array|float|int|string Number of permutations, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function PERMUTATIONA($numObjs, $numInSet) { - $numObjs = Functions::flattenSingleValue($numObjs); - $numInSet = Functions::flattenSingleValue($numInSet); + if (is_array($numObjs) || is_array($numInSet)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $numObjs, $numInSet); + } try { $numObjs = StatisticalValidations::validateInt($numObjs); @@ -67,7 +80,7 @@ class Permutations } if ($numObjs < 0 || $numInSet < 0) { - return Functions::NAN(); + return ExcelError::NAN(); } $result = $numObjs ** $numInSet; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Size.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Size.php index de4b6d6c3d8..2eef5fc7fbf 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Size.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Size.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Size { @@ -30,15 +31,15 @@ class Size $mArgs = self::filter($aArgs); $count = Counts::COUNT($mArgs); --$entry; - if (($entry < 0) || ($entry >= $count) || ($count == 0)) { - return Functions::NAN(); + if ($count === 0 || $entry < 0 || $entry >= $count) { + return ExcelError::NAN(); } rsort($mArgs); return $mArgs[$entry]; } - return Functions::VALUE(); + return ExcelError::VALUE(); } /** @@ -66,15 +67,15 @@ class Size $mArgs = self::filter($aArgs); $count = Counts::COUNT($mArgs); --$entry; - if (($entry < 0) || ($entry >= $count) || ($count == 0)) { - return Functions::NAN(); + if ($count === 0 || $entry < 0 || $entry >= $count) { + return ExcelError::NAN(); } sort($mArgs); return $mArgs[$entry]; } - return Functions::VALUE(); + return ExcelError::VALUE(); } /** diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Standardize.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Standardize.php index 2f3c58e72e3..51b0a51878f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Standardize.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Standardize.php @@ -2,27 +2,35 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Standardize extends StatisticalValidations { + use ArrayEnabled; + /** * STANDARDIZE. * * Returns a normalized value from a distribution characterized by mean and standard_dev. * - * @param float $value Value to normalize - * @param float $mean Mean Value - * @param float $stdDev Standard Deviation + * @param array|float $value Value to normalize + * Or can be an array of values + * @param array|float $mean Mean Value + * Or can be an array of values + * @param array|float $stdDev Standard Deviation + * Or can be an array of values * - * @return float|string Standardized value, or a string containing an error + * @return array|float|string Standardized value, or a string containing an error + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function execute($value, $mean, $stdDev) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); + if (is_array($value) || is_array($mean) || is_array($stdDev)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $mean, $stdDev); + } try { $value = self::validateFloat($value); @@ -33,7 +41,7 @@ class Standardize extends StatisticalValidations } if ($stdDev <= 0) { - return Functions::NAN(); + return ExcelError::NAN(); } return ($value - $mean) / $stdDev; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/StatisticalValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/StatisticalValidations.php index 5b315da45c9..e23a52ccd6d 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/StatisticalValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/StatisticalValidations.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Exception; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class StatisticalValidations { @@ -13,7 +13,7 @@ class StatisticalValidations public static function validateFloat($value): float { if (!is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (float) $value; @@ -25,7 +25,7 @@ class StatisticalValidations public static function validateInt($value): int { if (!is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (int) floor((float) $value); @@ -37,7 +37,7 @@ class StatisticalValidations public static function validateBool($value): bool { if (!is_bool($value) && !is_numeric($value)) { - throw new Exception(Functions::VALUE()); + throw new Exception(ExcelError::VALUE()); } return (bool) $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index 93724a8c1e7..af73519ef1e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -2,12 +2,16 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend; class Trends { + use ArrayEnabled; + private static function filterTrendValues(array &$array1, array &$array2): void { foreach ($array1 as $key => $value) { @@ -43,9 +47,9 @@ class Trends $xValueCount = count($xValues); if (($yValueCount === 0) || ($yValueCount !== $xValueCount)) { - throw new Exception(Functions::NA()); + throw new Exception(ExcelError::NA()); } elseif ($yValueCount === 1) { - throw new Exception(Functions::DIV0()); + throw new Exception(ExcelError::DIV0()); } } @@ -62,7 +66,7 @@ class Trends public static function CORREL($yValues, $xValues = null) { if (($xValues === null) || (!is_array($yValues)) || (!is_array($xValues))) { - return Functions::VALUE(); + return ExcelError::VALUE(); } try { @@ -108,14 +112,19 @@ class Trends * The predicted value is a y-value for a given x-value. * * @param mixed $xValue Float value of X for which we want to find Y + * Or can be an array of values * @param mixed $yValues array of mixed Data Series Y * @param mixed $xValues of mixed Data Series X * - * @return bool|float|string + * @return array|bool|float|string + * If an array of numbers is passed as an argument, then the returned result will also be an array + * with the same dimensions */ public static function FORECAST($xValue, $yValues, $xValues) { - $xValue = Functions::flattenSingleValue($xValue); + if (is_array($xValue)) { + return self::evaluateArrayArgumentsSubset([self::class, __FUNCTION__], 1, $xValue, $yValues, $xValues); + } try { $xValue = StatisticalValidations::validateFloat($xValue); @@ -224,7 +233,7 @@ class Trends ], [ $bestFitLinear->getSlopeSE(), - ($const === false) ? Functions::NA() : $bestFitLinear->getIntersectSE(), + ($const === false) ? ExcelError::NA() : $bestFitLinear->getIntersectSE(), ], [ $bestFitLinear->getGoodnessOfFit(), @@ -277,7 +286,7 @@ class Trends foreach ($yValues as $value) { if ($value < 0.0) { - return Functions::NAN(); + return ExcelError::NAN(); } } @@ -291,7 +300,7 @@ class Trends ], [ $bestFitExponential->getSlopeSE(), - ($const === false) ? Functions::NA() : $bestFitExponential->getIntersectSE(), + ($const === false) ? ExcelError::NA() : $bestFitExponential->getIntersectSE(), ], [ $bestFitExponential->getGoodnessOfFit(), diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Variances.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Variances.php index ac9c33209e3..35d01d1c577 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Variances.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Statistical/Variances.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Variances extends VarianceBase { @@ -20,7 +21,7 @@ class Variances extends VarianceBase */ public static function VAR(...$args) { - $returnValue = Functions::DIV0(); + $returnValue = ExcelError::DIV0(); $summerA = $summerB = 0.0; @@ -62,7 +63,7 @@ class Variances extends VarianceBase */ public static function VARA(...$args) { - $returnValue = Functions::DIV0(); + $returnValue = ExcelError::DIV0(); $summerA = $summerB = 0.0; @@ -71,7 +72,7 @@ class Variances extends VarianceBase $aCount = 0; foreach ($aArgs as $k => $arg) { if ((is_string($arg)) && (Functions::isValue($k))) { - return Functions::VALUE(); + return ExcelError::VALUE(); } elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { } else { // Is it a numeric value? @@ -109,7 +110,7 @@ class Variances extends VarianceBase public static function VARP(...$args) { // Return value - $returnValue = Functions::DIV0(); + $returnValue = ExcelError::DIV0(); $summerA = $summerB = 0.0; @@ -151,7 +152,7 @@ class Variances extends VarianceBase */ public static function VARPA(...$args) { - $returnValue = Functions::DIV0(); + $returnValue = ExcelError::DIV0(); $summerA = $summerB = 0.0; @@ -160,7 +161,7 @@ class Variances extends VarianceBase $aCount = 0; foreach ($aArgs as $k => $arg) { if ((is_string($arg)) && (Functions::isValue($k))) { - return Functions::VALUE(); + return ExcelError::VALUE(); } elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { } else { // Is it a numeric value? diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData.php index 0bde3b7fe48..6757a8d87c3 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData.php @@ -18,7 +18,7 @@ class TextData * * @param string $character Value * - * @return string + * @return array|string */ public static function CHARACTER($character) { @@ -34,7 +34,7 @@ class TextData * * @param mixed $stringValue Value to check * - * @return string + * @return null|array|string */ public static function TRIMNONPRINTABLE($stringValue = '') { @@ -50,7 +50,7 @@ class TextData * * @param mixed $stringValue Value to check * - * @return string + * @return array|string */ public static function TRIMSPACES($stringValue = '') { @@ -64,9 +64,9 @@ class TextData * * @see Use the code() method in the TextData\CharacterConvert class instead * - * @param string $characters Value + * @param array|string $characters Value * - * @return int|string A string if arguments are invalid + * @return array|int|string A string if arguments are invalid */ public static function ASCIICODE($characters) { @@ -102,7 +102,7 @@ class TextData * If decimals is negative, number is rounded to the left of the decimal point. * If you omit decimals, it is assumed to be 2 * - * @return string + * @return array|string */ public static function DOLLAR($value = 0, $decimals = 2) { @@ -110,17 +110,17 @@ class TextData } /** - * SEARCHSENSITIVE. + * FIND. * * @Deprecated 1.18.0 * * @see Use the sensitive() method in the TextData\Search class instead * - * @param string $needle The string to look for - * @param string $haystack The string in which to look - * @param int $offset Offset within $haystack + * @param array|string $needle The string to look for + * @param array|string $haystack The string in which to look + * @param array|int $offset Offset within $haystack * - * @return string + * @return array|int|string */ public static function SEARCHSENSITIVE($needle, $haystack, $offset = 1) { @@ -128,17 +128,17 @@ class TextData } /** - * SEARCHINSENSITIVE. + * SEARCH. * * @Deprecated 1.18.0 * * @see Use the insensitive() method in the TextData\Search class instead * - * @param string $needle The string to look for - * @param string $haystack The string in which to look - * @param int $offset Offset within $haystack + * @param array|string $needle The string to look for + * @param array|string $haystack The string in which to look + * @param array|int $offset Offset within $haystack * - * @return string + * @return array|int|string */ public static function SEARCHINSENSITIVE($needle, $haystack, $offset = 1) { @@ -156,7 +156,7 @@ class TextData * @param int $decimals * @param bool $no_commas * - * @return string + * @return array|string */ public static function FIXEDFORMAT($value, $decimals = 2, $no_commas = false) { @@ -170,10 +170,10 @@ class TextData * * @see Use the left() method in the TextData\Extract class instead * - * @param string $value Value - * @param int $chars Number of characters + * @param array|string $value Value + * @param array|int $chars Number of characters * - * @return string + * @return array|string */ public static function LEFT($value = '', $chars = 1) { @@ -187,11 +187,11 @@ class TextData * * @see Use the mid() method in the TextData\Extract class instead * - * @param string $value Value - * @param int $start Start character - * @param int $chars Number of characters + * @param array|string $value Value + * @param array|int $start Start character + * @param array|int $chars Number of characters * - * @return string + * @return array|string */ public static function MID($value = '', $start = 1, $chars = null) { @@ -205,10 +205,10 @@ class TextData * * @see Use the right() method in the TextData\Extract class instead * - * @param string $value Value - * @param int $chars Number of characters + * @param array|string $value Value + * @param array|int $chars Number of characters * - * @return string + * @return array|string */ public static function RIGHT($value = '', $chars = 1) { @@ -224,7 +224,7 @@ class TextData * * @param string $value Value * - * @return int + * @return array|int */ public static function STRINGLENGTH($value = '') { @@ -234,15 +234,15 @@ class TextData /** * LOWERCASE. * - * Converts a string value to upper case. + * Converts a string value to lower case. * * @Deprecated 1.18.0 * * @see Use the lower() method in the TextData\CaseConvert class instead * - * @param string $mixedCaseString + * @param array|string $mixedCaseString * - * @return string + * @return array|string */ public static function LOWERCASE($mixedCaseString) { @@ -260,7 +260,7 @@ class TextData * * @param string $mixedCaseString * - * @return string + * @return array|string */ public static function UPPERCASE($mixedCaseString) { @@ -270,15 +270,15 @@ class TextData /** * PROPERCASE. * - * Converts a string value to upper case. + * Converts a string value to proper/title case. * * @Deprecated 1.18.0 * * @see Use the proper() method in the TextData\CaseConvert class instead * - * @param string $mixedCaseString + * @param array|string $mixedCaseString * - * @return string + * @return array|string */ public static function PROPERCASE($mixedCaseString) { @@ -297,7 +297,7 @@ class TextData * @param int $chars Number of characters * @param string $newText String to replace in defined position * - * @return string + * @return array|string */ public static function REPLACE($oldText, $start, $chars, $newText) { @@ -316,7 +316,7 @@ class TextData * @param string $toText To Value * @param int $instance Instance Number * - * @return string + * @return array|string */ public static function SUBSTITUTE($text = '', $fromText = '', $toText = '', $instance = 0) { @@ -332,7 +332,7 @@ class TextData * * @param mixed $testValue Value to check * - * @return null|string + * @return null|array|string */ public static function RETURNSTRING($testValue = '') { @@ -349,7 +349,7 @@ class TextData * @param mixed $value Value to check * @param string $format Format mask to use * - * @return string + * @return array|string */ public static function TEXTFORMAT($value, $format) { @@ -365,7 +365,7 @@ class TextData * * @param mixed $value Value to check * - * @return DateTimeInterface|float|int|string A string if arguments are invalid + * @return array|DateTimeInterface|float|int|string A string if arguments are invalid */ public static function VALUE($value = '') { @@ -383,7 +383,7 @@ class TextData * @param string $decimalSeparator decimal separator, defaults to locale defined value * @param string $groupSeparator group/thosands separator, defaults to locale defined value * - * @return float|string + * @return array|float|string */ public static function NUMBERVALUE($value = '', $decimalSeparator = null, $groupSeparator = null) { @@ -402,7 +402,7 @@ class TextData * @param mixed $value1 * @param mixed $value2 * - * @return bool + * @return array|bool */ public static function EXACT($value1, $value2) { @@ -420,7 +420,7 @@ class TextData * @param mixed $ignoreEmpty * @param mixed $args * - * @return string + * @return array|string */ public static function TEXTJOIN($delimiter, $ignoreEmpty, ...$args) { @@ -436,10 +436,10 @@ class TextData * * @see Use the builtinREPT() method in the TextData\Concatenate class instead * - * @param string $str Should be numeric + * @param array|string $str Should be numeric * @param mixed $number Should be int * - * @return string + * @return array|string */ public static function builtinREPT($str, $number) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php index 664cc2d883f..f1aea16940f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php @@ -2,21 +2,31 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class CaseConvert { + use ArrayEnabled; + /** * LOWERCASE. * * Converts a string value to upper case. * * @param mixed $mixedCaseValue The string value to convert to lower case + * Or can be an array of values + * + * @return array|string + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ - public static function lower($mixedCaseValue): string + public static function lower($mixedCaseValue) { - $mixedCaseValue = Functions::flattenSingleValue($mixedCaseValue); + if (is_array($mixedCaseValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); + } + $mixedCaseValue = Helpers::extractString($mixedCaseValue); return StringHelper::strToLower($mixedCaseValue); @@ -28,10 +38,18 @@ class CaseConvert * Converts a string value to upper case. * * @param mixed $mixedCaseValue The string value to convert to upper case + * Or can be an array of values + * + * @return array|string + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ - public static function upper($mixedCaseValue): string + public static function upper($mixedCaseValue) { - $mixedCaseValue = Functions::flattenSingleValue($mixedCaseValue); + if (is_array($mixedCaseValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); + } + $mixedCaseValue = Helpers::extractString($mixedCaseValue); return StringHelper::strToUpper($mixedCaseValue); @@ -43,10 +61,18 @@ class CaseConvert * Converts a string value to proper or title case. * * @param mixed $mixedCaseValue The string value to convert to title case + * Or can be an array of values + * + * @return array|string + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ - public static function proper($mixedCaseValue): string + public static function proper($mixedCaseValue) { - $mixedCaseValue = Functions::flattenSingleValue($mixedCaseValue); + if (is_array($mixedCaseValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $mixedCaseValue); + } + $mixedCaseValue = Helpers::extractString($mixedCaseValue); return StringHelper::strToTitle($mixedCaseValue); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php index 0aca57e6ce9..83af499bbf4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php @@ -2,21 +2,34 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class CharacterConvert { + use ArrayEnabled; + /** * CHAR. * * @param mixed $character Integer Value to convert to its character representation + * Or can be an array of values + * + * @return array|string The character string + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ - public static function character($character): string + public static function character($character) { + if (is_array($character)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $character); + } + $character = Helpers::validateInt($character); $min = Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE ? 0 : 1; if ($character < $min || $character > 255) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $result = iconv('UCS-4LE', 'UTF-8', pack('V', $character)); @@ -27,14 +40,21 @@ class CharacterConvert * CODE. * * @param mixed $characters String character to convert to its ASCII value + * Or can be an array of values * - * @return int|string A string if arguments are invalid + * @return array|int|string A string if arguments are invalid + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function code($characters) { + if (is_array($characters)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $characters); + } + $characters = Helpers::extractString($characters); if ($characters === '') { - return Functions::VALUE(); + return ExcelError::VALUE(); } $character = $characters; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php index d53fc822898..7bd60e90f2b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php @@ -2,10 +2,17 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Concatenate { + use ArrayEnabled; + /** * CONCATENATE. * @@ -19,7 +26,18 @@ class Concatenate $aArgs = Functions::flattenArray($args); foreach ($aArgs as $arg) { + $value = Helpers::extractString($arg); + if (ErrorValue::isError($value)) { + $returnValue = $value; + + break; + } $returnValue .= Helpers::extractString($arg); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + + break; + } } return $returnValue; @@ -28,17 +46,38 @@ class Concatenate /** * TEXTJOIN. * - * @param mixed $delimiter - * @param mixed $ignoreEmpty - * @param mixed $args + * @param mixed $delimiter The delimter to use between the joined arguments + * Or can be an array of values + * @param mixed $ignoreEmpty true/false Flag indicating whether empty arguments should be skipped + * Or can be an array of values + * @param mixed $args The values to join + * + * @return array|string The joined string + * If an array of values is passed for the $delimiter or $ignoreEmpty arguments, then the returned result + * will also be an array with matching dimensions */ - public static function TEXTJOIN($delimiter, $ignoreEmpty, ...$args): string + public static function TEXTJOIN($delimiter, $ignoreEmpty, ...$args) { - $delimiter = Functions::flattenSingleValue($delimiter); - $ignoreEmpty = Functions::flattenSingleValue($ignoreEmpty); + if (is_array($delimiter) || is_array($ignoreEmpty)) { + return self::evaluateArrayArgumentsSubset( + [self::class, __FUNCTION__], + 2, + $delimiter, + $ignoreEmpty, + ...$args + ); + } + // Loop through arguments $aArgs = Functions::flattenArray($args); + $returnValue = ''; foreach ($aArgs as $key => &$arg) { + $value = Helpers::extractString($arg); + if (ErrorValue::isError($value)) { + $returnValue = $value; + + break; + } if ($ignoreEmpty === true && is_string($arg) && trim($arg) === '') { unset($aArgs[$key]); } elseif (is_bool($arg)) { @@ -46,7 +85,12 @@ class Concatenate } } - return implode($delimiter, $aArgs); + $returnValue = ($returnValue !== '') ? $returnValue : implode($delimiter, $aArgs); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::CALC(); + } + + return $returnValue; } /** @@ -55,17 +99,33 @@ class Concatenate * Returns the result of builtin function round after validating args. * * @param mixed $stringValue The value to repeat + * Or can be an array of values * @param mixed $repeatCount The number of times the string value should be repeated + * Or can be an array of values + * + * @return array|string The repeated string + * If an array of values is passed for the $stringValue or $repeatCount arguments, then the returned result + * will also be an array with matching dimensions */ - public static function builtinREPT($stringValue, $repeatCount): string + public static function builtinREPT($stringValue, $repeatCount) { - $repeatCount = Functions::flattenSingleValue($repeatCount); + if (is_array($stringValue) || is_array($repeatCount)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $stringValue, $repeatCount); + } + $stringValue = Helpers::extractString($stringValue); if (!is_numeric($repeatCount) || $repeatCount < 0) { - return Functions::VALUE(); + $returnValue = ExcelError::VALUE(); + } elseif (ErrorValue::isError($stringValue)) { + $returnValue = $stringValue; + } else { + $returnValue = str_repeat($stringValue, (int) $repeatCount); + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); // note VALUE not CALC + } } - return str_repeat($stringValue, (int) $repeatCount); + return $returnValue; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 7f18e0c6e7c..ee7e31b773f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -2,18 +2,34 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Extract { + use ArrayEnabled; + /** * LEFT. * * @param mixed $value String value from which to extract characters + * Or can be an array of values * @param mixed $chars The number of characters to extract (as an integer) + * Or can be an array of values + * + * @return array|string The joined string + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions */ - public static function left($value, $chars = 1): string + public static function left($value, $chars = 1) { + if (is_array($value) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $chars); + } + try { $value = Helpers::extractString($value); $chars = Helpers::extractInt($chars, 0, 1); @@ -28,11 +44,22 @@ class Extract * MID. * * @param mixed $value String value from which to extract characters + * Or can be an array of values * @param mixed $start Integer offset of the first character that we want to extract + * Or can be an array of values * @param mixed $chars The number of characters to extract (as an integer) + * Or can be an array of values + * + * @return array|string The joined string + * If an array of values is passed for the $value, $start or $chars arguments, then the returned result + * will also be an array with matching dimensions */ - public static function mid($value, $start, $chars): string + public static function mid($value, $start, $chars) { + if (is_array($value) || is_array($start) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $start, $chars); + } + try { $value = Helpers::extractString($value); $start = Helpers::extractInt($start, 1); @@ -48,10 +75,20 @@ class Extract * RIGHT. * * @param mixed $value String value from which to extract characters + * Or can be an array of values * @param mixed $chars The number of characters to extract (as an integer) + * Or can be an array of values + * + * @return array|string The joined string + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions */ - public static function right($value, $chars = 1): string + public static function right($value, $chars = 1) { + if (is_array($value) || is_array($chars)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $chars); + } + try { $value = Helpers::extractString($value); $chars = Helpers::extractInt($chars, 0, 1); @@ -61,4 +98,183 @@ class Extract return mb_substr($value ?? '', mb_strlen($value ?? '', 'UTF-8') - $chars, $chars, 'UTF-8'); } + + /** + * TEXTBEFORE. + * + * @param mixed $text the text that you're searching + * Or can be an array of values + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param mixed $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function before($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A') + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + $text = Helpers::extractString($text ?? ''); + $instance = (int) $instance; + $matchMode = (int) $matchMode; + $matchEnd = (int) $matchEnd; + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_array($split) === false) { + return $split; + } + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { + return ($instance > 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, 0, max(count($split) - (abs($instance) * 2 - 1) - $adjust - $oddReverseAdjustment, 0)) + : array_slice($split, 0, $instance * 2 - 1 - $adjust); + + return implode('', $split); + } + + /** + * TEXTAFTER. + * + * @param mixed $text the text that you're searching + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + * @param mixed $instance The instance of the delimiter after which you want to extract the text. + * By default, this is the first instance (1). + * A negative value means start searching from the end of the text string. + * Or can be an array of values + * @param mixed $matchMode Determines whether the match is case-sensitive or not. + * 0 - Case-sensitive + * 1 - Case-insensitive + * Or can be an array of values + * @param mixed $matchEnd Treats the end of text as a delimiter. + * 0 - Don't match the delimiter against the end of the text. + * 1 - Match the delimiter against the end of the text. + * Or can be an array of values + * @param mixed $ifNotFound value to return if no match is found + * The default is a #N/A Error + * Or can be an array of values + * + * @return mixed|mixed[] the string extracted from text before the delimiter; or the $ifNotFound value + * If an array of values is passed for any of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function after($text, $delimiter, $instance = 1, $matchMode = 0, $matchEnd = 0, $ifNotFound = '#N/A') + { + if (is_array($text) || is_array($instance) || is_array($matchMode) || is_array($matchEnd) || is_array($ifNotFound)) { + return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + } + + $text = Helpers::extractString($text ?? ''); + $instance = (int) $instance; + $matchMode = (int) $matchMode; + $matchEnd = (int) $matchEnd; + + $split = self::validateTextBeforeAfter($text, $delimiter, $instance, $matchMode, $matchEnd, $ifNotFound); + if (is_array($split) === false) { + return $split; + } + if (Helpers::extractString(Functions::flattenSingleValue($delimiter ?? '')) === '') { + return ($instance < 0) ? '' : $text; + } + + // Adjustment for a match as the first element of the split + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + $adjust = preg_match('/^' . $delimiter . "\$/{$flags}", $split[0]); + $oddReverseAdjustment = count($split) % 2; + + $split = ($instance < 0) + ? array_slice($split, count($split) - (abs($instance + 1) * 2) - $adjust - $oddReverseAdjustment) + : array_slice($split, $instance * 2 - $adjust); + + return implode('', $split); + } + + /** + * @param null|array|string $delimiter + * @param int $matchMode + * @param int $matchEnd + * @param mixed $ifNotFound + * + * @return string|string[] + */ + private static function validateTextBeforeAfter(string $text, $delimiter, int $instance, $matchMode, $matchEnd, $ifNotFound) + { + $flags = self::matchFlags($matchMode); + $delimiter = self::buildDelimiter($delimiter); + + if (preg_match('/' . $delimiter . "/{$flags}", $text) === 0 && $matchEnd === 0) { + return $ifNotFound; + } + + $split = preg_split('/' . $delimiter . "/{$flags}", $text, 0, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + if ($split === false) { + return ExcelError::NA(); + } + + if ($instance === 0 || abs($instance) > StringHelper::countCharacters($text)) { + return ExcelError::VALUE(); + } + + if ($matchEnd === 0 && (abs($instance) > floor(count($split) / 2))) { + return ExcelError::NA(); + } elseif ($matchEnd !== 0 && (abs($instance) - 1 > ceil(count($split) / 2))) { + return ExcelError::NA(); + } + + return $split; + } + + /** + * @param null|array|string $delimiter the text that marks the point before which you want to extract + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + if (is_array($delimiter)) { + $delimiter = Functions::flattenArray($delimiter); + $quotedDelimiters = array_map( + function ($delimiter) { + return preg_quote($delimiter ?? ''); + }, + $delimiter + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote($delimiter ?? '') . ')'; + } + + private static function matchFlags(int $matchMode): string + { + return ($matchMode === 0) ? 'mu' : 'miu'; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php index 3286de0cb03..03e75d1d22e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -3,16 +3,22 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use DateTimeInterface; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; class Format { + use ArrayEnabled; + /** * DOLLAR. * @@ -20,12 +26,22 @@ class Format * The format used is $#,##0.00_);($#,##0.00).. * * @param mixed $value The value to format + * Or can be an array of values * @param mixed $decimals The number of digits to display to the right of the decimal point (as an integer). * If decimals is negative, number is rounded to the left of the decimal point. * If you omit decimals, it is assumed to be 2 + * Or can be an array of values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions */ - public static function DOLLAR($value = 0, $decimals = 2): string + public static function DOLLAR($value = 0, $decimals = 2) { + if (is_array($value) || is_array($decimals)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimals); + } + try { $value = Helpers::extractFloat($value); $decimals = Helpers::extractInt($decimals, -100, 0, true); @@ -52,15 +68,25 @@ class Format * FIXED. * * @param mixed $value The value to format + * Or can be an array of values * @param mixed $decimals Integer value for the number of decimal places that should be formatted + * Or can be an array of values * @param mixed $noCommas Boolean value indicating whether the value should have thousands separators or not + * Or can be an array of values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions */ - public static function FIXEDFORMAT($value, $decimals = 2, $noCommas = false): string + public static function FIXEDFORMAT($value, $decimals = 2, $noCommas = false) { + if (is_array($value) || is_array($decimals) || is_array($noCommas)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimals, $noCommas); + } + try { $value = Helpers::extractFloat($value); $decimals = Helpers::extractInt($decimals, -100, 0, true); - $noCommas = Functions::flattenSingleValue($noCommas); } catch (CalcExp $e) { return $e->getMessage(); } @@ -85,10 +111,20 @@ class Format * TEXT. * * @param mixed $value The value to format + * Or can be an array of values * @param mixed $format A string with the Format mask that should be used + * Or can be an array of values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions */ - public static function TEXTFORMAT($value, $format): string + public static function TEXTFORMAT($value, $format) { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + $value = Helpers::extractString($value); $format = Helpers::extractString($format); @@ -106,12 +142,12 @@ class Format */ private static function convertValue($value) { - $value = ($value === null) ? 0 : Functions::flattenSingleValue($value); + $value = $value ?? 0; if (is_bool($value)) { if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { $value = (int) $value; } else { - throw new CalcExp(Functions::VALUE()); + throw new CalcExp(ExcelError::VALUE()); } } @@ -122,11 +158,18 @@ class Format * VALUE. * * @param mixed $value Value to check + * Or can be an array of values * - * @return DateTimeInterface|float|int|string A string if arguments are invalid + * @return array|DateTimeInterface|float|int|string A string if arguments are invalid + * If an array of values is passed for the argument, then the returned result + * will also be an array with matching dimensions */ public static function VALUE($value = '') { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + try { $value = self::convertValue($value); } catch (CalcExp $e) { @@ -146,34 +189,64 @@ class Format Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); if (strpos($value, ':') !== false) { - $timeValue = DateTimeExcel\TimeValue::fromString($value); - if ($timeValue !== Functions::VALUE()) { + $timeValue = Functions::scalar(DateTimeExcel\TimeValue::fromString($value)); + if ($timeValue !== ExcelError::VALUE()) { Functions::setReturnDateType($dateSetting); return $timeValue; } } - $dateValue = DateTimeExcel\DateValue::fromString($value); - if ($dateValue !== Functions::VALUE()) { + $dateValue = Functions::scalar(DateTimeExcel\DateValue::fromString($value)); + if ($dateValue !== ExcelError::VALUE()) { Functions::setReturnDateType($dateSetting); return $dateValue; } Functions::setReturnDateType($dateSetting); - return Functions::VALUE(); + return ExcelError::VALUE(); } return (float) $value; } + /** + * TEXT. + * + * @param mixed $value The value to format + * Or can be an array of values + * @param mixed $format + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions + */ + public static function valueToText($value, $format = false) + { + if (is_array($value) || is_array($format)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $format); + } + + $format = (bool) $format; + + if (is_object($value) && $value instanceof RichText) { + $value = $value->getPlainText(); + } + if (is_string($value)) { + $value = ($format === true) ? Calculation::wrapResult($value) : $value; + $value = str_replace("\n", '', $value); + } elseif (is_bool($value)) { + $value = Calculation::$localeBoolean[$value === true ? 'TRUE' : 'FALSE']; + } + + return (string) $value; + } + /** * @param mixed $decimalSeparator */ private static function getDecimalSeparator($decimalSeparator): string { - $decimalSeparator = Functions::flattenSingleValue($decimalSeparator); - return empty($decimalSeparator) ? StringHelper::getDecimalSeparator() : (string) $decimalSeparator; } @@ -182,8 +255,6 @@ class Format */ private static function getGroupSeparator($groupSeparator): string { - $groupSeparator = Functions::flattenSingleValue($groupSeparator); - return empty($groupSeparator) ? StringHelper::getThousandsSeparator() : (string) $groupSeparator; } @@ -191,13 +262,20 @@ class Format * NUMBERVALUE. * * @param mixed $value The value to format + * Or can be an array of values * @param mixed $decimalSeparator A string with the decimal separator to use, defaults to locale defined value + * Or can be an array of values * @param mixed $groupSeparator A string with the group/thousands separator to use, defaults to locale defined value + * Or can be an array of values * - * @return float|string + * @return array|float|string */ public static function NUMBERVALUE($value = '', $decimalSeparator = null, $groupSeparator = null) { + if (is_array($value) || is_array($decimalSeparator) || is_array($groupSeparator)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $decimalSeparator, $groupSeparator); + } + try { $value = self::convertValue($value); $decimalSeparator = self::getDecimalSeparator($decimalSeparator); @@ -209,11 +287,11 @@ class Format if (!is_numeric($value)) { $decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator) . '/', $value, $matches, PREG_OFFSET_CAPTURE); if ($decimalPositions > 1) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $decimalOffset = array_pop($matches[0])[1]; if (strpos($value, $groupSeparator, $decimalOffset) !== false) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $value = str_replace([$groupSeparator, $decimalSeparator], ['', '.'], $value); @@ -221,7 +299,7 @@ class Format // Handle the special case of trailing % signs $percentageString = rtrim($value, '%'); if (!is_numeric($percentageString)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } $percentageAdjustment = strlen($value) - strlen($percentageString); @@ -231,6 +309,6 @@ class Format } } - return is_array($value) ? Functions::VALUE() : (float) $value; + return is_array($value) ? ExcelError::VALUE() : (float) $value; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Helpers.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Helpers.php index 423b6d3f90b..e7b67a34034 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Helpers.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Helpers.php @@ -5,6 +5,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; class Helpers { @@ -20,12 +22,14 @@ class Helpers /** * @param mixed $value String value from which to extract characters */ - public static function extractString($value): string + public static function extractString($value, bool $throwIfError = false): string { - $value = Functions::flattenSingleValue($value); if (is_bool($value)) { return self::convertBooleanValue($value); } + if ($throwIfError && is_string($value) && ErrorValue::isError($value)) { + throw new CalcExp($value); + } return (string) $value; } @@ -35,7 +39,6 @@ class Helpers */ public static function extractInt($value, int $minValue, int $gnumericNull = 0, bool $ooBoolOk = false): int { - $value = Functions::flattenSingleValue($value); if ($value === null) { // usually 0, but sometimes 1 for Gnumeric $value = (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_GNUMERIC) ? $gnumericNull : 0; @@ -44,11 +47,11 @@ class Helpers $value = (int) $value; } if (!is_numeric($value)) { - throw new CalcExp(Functions::VALUE()); + throw new CalcExp(ExcelError::VALUE()); } $value = (int) $value; if ($value < $minValue) { - throw new CalcExp(Functions::VALUE()); + throw new CalcExp(ExcelError::VALUE()); } return (int) $value; @@ -59,7 +62,6 @@ class Helpers */ public static function extractFloat($value): float { - $value = Functions::flattenSingleValue($value); if ($value === null) { $value = 0.0; } @@ -67,7 +69,7 @@ class Helpers $value = (float) $value; } if (!is_numeric($value)) { - throw new CalcExp(Functions::VALUE()); + throw new CalcExp(ExcelError::VALUE()); } return (float) $value; @@ -78,7 +80,6 @@ class Helpers */ public static function validateInt($value): int { - $value = Functions::flattenSingleValue($value); if ($value === null) { $value = 0; } elseif (is_bool($value)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Replace.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Replace.php index 36c13590e9a..03b663211ed 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Replace.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Replace.php @@ -2,65 +2,111 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Replace { + use ArrayEnabled; + /** * REPLACE. * * @param mixed $oldText The text string value to modify + * Or can be an array of values * @param mixed $start Integer offset for start character of the replacement + * Or can be an array of values * @param mixed $chars Integer number of characters to replace from the start offset + * Or can be an array of values * @param mixed $newText String to replace in the defined position + * Or can be an array of values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions */ - public static function replace($oldText, $start, $chars, $newText): string + public static function replace($oldText, $start, $chars, $newText) { + if (is_array($oldText) || is_array($start) || is_array($chars) || is_array($newText)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $oldText, $start, $chars, $newText); + } + try { $start = Helpers::extractInt($start, 1, 0, true); $chars = Helpers::extractInt($chars, 0, 0, true); - $oldText = Helpers::extractString($oldText); - $newText = Helpers::extractString($newText); - $left = mb_substr($oldText, 0, $start - 1, 'UTF-8'); + $oldText = Helpers::extractString($oldText, true); + $newText = Helpers::extractString($newText, true); + $left = StringHelper::substring($oldText, 0, $start - 1); - $right = mb_substr($oldText, $start + $chars - 1, null, 'UTF-8'); + $right = StringHelper::substring($oldText, $start + $chars - 1, null); } catch (CalcExp $e) { return $e->getMessage(); } + $returnValue = $left . $newText . $right; + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } - return $left . $newText . $right; + return $returnValue; } /** * SUBSTITUTE. * * @param mixed $text The text string value to modify + * Or can be an array of values * @param mixed $fromText The string value that we want to replace in $text + * Or can be an array of values * @param mixed $toText The string value that we want to replace with in $text + * Or can be an array of values * @param mixed $instance Integer instance Number for the occurrence of frmText to change + * Or can be an array of values + * + * @return array|string + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions */ - public static function substitute($text = '', $fromText = '', $toText = '', $instance = null): string + public static function substitute($text = '', $fromText = '', $toText = '', $instance = null) { + if (is_array($text) || is_array($fromText) || is_array($toText) || is_array($instance)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $text, $fromText, $toText, $instance); + } + try { - $text = Helpers::extractString($text); - $fromText = Helpers::extractString($fromText); - $toText = Helpers::extractString($toText); - $instance = Functions::flattenSingleValue($instance); + $text = Helpers::extractString($text, true); + $fromText = Helpers::extractString($fromText, true); + $toText = Helpers::extractString($toText, true); if ($instance === null) { - return str_replace($fromText, $toText, $text); - } - if (is_bool($instance)) { - if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { - return Functions::Value(); + $returnValue = str_replace($fromText, $toText, $text); + } else { + if (is_bool($instance)) { + if ($instance === false || Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { + return ExcelError::Value(); + } + $instance = 1; } - $instance = 1; + $instance = Helpers::extractInt($instance, 1, 0, true); + $returnValue = self::executeSubstitution($text, $fromText, $toText, $instance); } - $instance = Helpers::extractInt($instance, 1, 0, true); } catch (CalcExp $e) { return $e->getMessage(); } + if (StringHelper::countCharacters($returnValue) > DataType::MAX_STRING_LENGTH) { + $returnValue = ExcelError::VALUE(); + } + return $returnValue; + } + + /** + * @return string + */ + private static function executeSubstitution(string $text, string $fromText, string $toText, int $instance) + { $pos = -1; while ($instance > 0) { $pos = mb_strpos($text, $fromText, $pos + 1, 'UTF-8'); @@ -71,7 +117,7 @@ class Replace } if ($pos !== false) { - return self::REPLACE($text, ++$pos, mb_strlen($fromText, 'UTF-8'), $toText); + return Functions::scalar(self::REPLACE($text, ++$pos, StringHelper::countCharacters($fromText), $toText)); } return $text; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php index c9eed2e5a5a..10b6a1aadf9 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Search.php @@ -2,23 +2,35 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class Search { + use ArrayEnabled; + /** * FIND (case sensitive search). * * @param mixed $needle The string to look for + * Or can be an array of values * @param mixed $haystack The string in which to look + * Or can be an array of values * @param mixed $offset Integer offset within $haystack to start searching from + * Or can be an array of values * - * @return int|string + * @return array|int|string The offset where the first occurrence of needle was found in the haystack + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions */ public static function sensitive($needle, $haystack, $offset = 1) { + if (is_array($needle) || is_array($haystack) || is_array($offset)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $needle, $haystack, $offset); + } + try { $needle = Helpers::extractString($needle); $haystack = Helpers::extractString($haystack); @@ -38,20 +50,29 @@ class Search } } - return Functions::VALUE(); + return ExcelError::VALUE(); } /** * SEARCH (case insensitive search). * * @param mixed $needle The string to look for + * Or can be an array of values * @param mixed $haystack The string in which to look + * Or can be an array of values * @param mixed $offset Integer offset within $haystack to start searching from + * Or can be an array of values * - * @return int|string + * @return array|int|string The offset where the first occurrence of needle was found in the haystack + * If an array of values is passed for the $value or $chars arguments, then the returned result + * will also be an array with matching dimensions */ public static function insensitive($needle, $haystack, $offset = 1) { + if (is_array($needle) || is_array($haystack) || is_array($offset)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $needle, $haystack, $offset); + } + try { $needle = Helpers::extractString($needle); $haystack = Helpers::extractString($haystack); @@ -71,6 +92,6 @@ class Search } } - return Functions::VALUE(); + return ExcelError::VALUE(); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php index 6f8253eabe8..83810422cc3 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -2,17 +2,30 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Text { + use ArrayEnabled; + /** * LEN. * * @param mixed $value String Value + * Or can be an array of values + * + * @return array|int + * If an array of values is passed for the argument, then the returned result + * will also be an array with matching dimensions */ - public static function length($value = ''): int + public static function length($value = '') { + if (is_array($value)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $value); + } + $value = Helpers::extractString($value); return mb_strlen($value ?? '', 'UTF-8'); @@ -24,10 +37,20 @@ class Text * Use EXACT to test text being entered into a document. * * @param mixed $value1 String Value + * Or can be an array of values * @param mixed $value2 String Value + * Or can be an array of values + * + * @return array|bool + * If an array of values is passed for either of the arguments, then the returned result + * will also be an array with matching dimensions */ - public static function exact($value1, $value2): bool + public static function exact($value1, $value2) { + if (is_array($value1) || is_array($value2)) { + return self::evaluateArrayArguments([self::class, __FUNCTION__], $value1, $value2); + } + $value1 = Helpers::extractString($value1); $value2 = Helpers::extractString($value2); @@ -38,12 +61,17 @@ class Text * RETURNSTRING. * * @param mixed $testValue Value to check + * Or can be an array of values * - * @return null|string + * @return null|array|string + * If an array of values is passed for the argument, then the returned result + * will also be an array with matching dimensions */ public static function test($testValue = '') { - $testValue = Functions::flattenSingleValue($testValue); + if (is_array($testValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $testValue); + } if (is_string($testValue)) { return $testValue; @@ -51,4 +79,176 @@ class Text return null; } + + /** + * TEXTSPLIT. + * + * @param mixed $text the text that you're searching + * @param null|array|string $columnDelimiter The text that marks the point where to spill the text across columns. + * Multiple delimiters can be passed as an array of string values + * @param null|array|string $rowDelimiter The text that marks the point where to spill the text down rows. + * Multiple delimiters can be passed as an array of string values + * @param bool $ignoreEmpty Specify FALSE to create an empty cell when two delimiters are consecutive. + * true = create empty cells + * false = skip empty cells + * Defaults to TRUE, which creates an empty cell + * @param bool $matchMode Determines whether the match is case-sensitive or not. + * true = case-sensitive + * false = case-insensitive + * By default, a case-sensitive match is done. + * @param mixed $padding The value with which to pad the result. + * The default is #N/A. + * + * @return array the array built from the text, split by the row and column delimiters + */ + public static function split($text, $columnDelimiter = null, $rowDelimiter = null, bool $ignoreEmpty = false, bool $matchMode = true, $padding = '#N/A') + { + $text = Functions::flattenSingleValue($text); + + $flags = self::matchFlags($matchMode); + + if ($rowDelimiter !== null) { + $delimiter = self::buildDelimiter($rowDelimiter); + $rows = ($delimiter === '()') + ? [$text] + : preg_split("/{$delimiter}/{$flags}", $text); + } else { + $rows = [$text]; + } + + /** @var array $rows */ + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + function ($row) { + return $row !== ''; + } + )); + } + + if ($columnDelimiter !== null) { + $delimiter = self::buildDelimiter($columnDelimiter); + array_walk( + $rows, + function (&$row) use ($delimiter, $flags, $ignoreEmpty): void { + $row = ($delimiter === '()') + ? [$row] + : preg_split("/{$delimiter}/{$flags}", $row); + /** @var array $row */ + if ($ignoreEmpty === true) { + $row = array_values(array_filter( + $row, + function ($value) { + return $value !== ''; + } + )); + } + } + ); + if ($ignoreEmpty === true) { + $rows = array_values(array_filter( + $rows, + function ($row) { + return $row !== [] && $row !== ['']; + } + )); + } + } + + return self::applyPadding($rows, $padding); + } + + /** + * @param mixed $padding + */ + private static function applyPadding(array $rows, $padding): array + { + $columnCount = array_reduce( + $rows, + function (int $counter, array $row): int { + return max($counter, count($row)); + }, + 0 + ); + + return array_map( + function (array $row) use ($columnCount, $padding): array { + return (count($row) < $columnCount) + ? array_merge($row, array_fill(0, $columnCount - count($row), $padding)) + : $row; + }, + $rows + ); + } + + /** + * @param null|array|string $delimiter the text that marks the point before which you want to split + * Multiple delimiters can be passed as an array of string values + */ + private static function buildDelimiter($delimiter): string + { + $valueSet = Functions::flattenArray($delimiter); + + if (is_array($delimiter) && count($valueSet) > 1) { + $quotedDelimiters = array_map( + function ($delimiter) { + return preg_quote($delimiter ?? ''); + }, + $valueSet + ); + $delimiters = implode('|', $quotedDelimiters); + + return '(' . $delimiters . ')'; + } + + return '(' . preg_quote(Functions::flattenSingleValue($delimiter)) . ')'; + } + + private static function matchFlags(bool $matchMode): string + { + return ($matchMode === true) ? 'miu' : 'mu'; + } + + public static function fromArray(array $array, int $format = 0): string + { + $result = []; + foreach ($array as $row) { + $cells = []; + foreach ($row as $cellValue) { + $value = ($format === 1) ? self::formatValueMode1($cellValue) : self::formatValueMode0($cellValue); + $cells[] = $value; + } + $result[] = implode(($format === 1) ? ',' : ', ', $cells); + } + + $result = implode(($format === 1) ? ';' : ', ', $result); + + return ($format === 1) ? '{' . $result . '}' : $result; + } + + /** + * @param mixed $cellValue + */ + private static function formatValueMode0($cellValue): string + { + if (is_bool($cellValue)) { + return ($cellValue) ? Calculation::$localeBoolean['TRUE'] : Calculation::$localeBoolean['FALSE']; + } + + return (string) $cellValue; + } + + /** + * @param mixed $cellValue + */ + private static function formatValueMode1($cellValue): string + { + if (is_string($cellValue) && Functions::isError($cellValue) === false) { + return Calculation::FORMULA_STRING_QUOTE . $cellValue . Calculation::FORMULA_STRING_QUOTE; + } elseif (is_bool($cellValue)) { + return ($cellValue) ? Calculation::$localeBoolean['TRUE'] : Calculation::$localeBoolean['FALSE']; + } + + return (string) $cellValue; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php index 22f4554b335..27eceb93884 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/TextData/Trim.php @@ -2,31 +2,49 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled; + class Trim { + use ArrayEnabled; + /** * CLEAN. * * @param mixed $stringValue String Value to check + * Or can be an array of values * - * @return null|string + * @return array|string + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function nonPrintable($stringValue = '') { + if (is_array($stringValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $stringValue); + } + $stringValue = Helpers::extractString($stringValue); - return preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); + return (string) preg_replace('/[\\x00-\\x1f]/', '', "$stringValue"); } /** * TRIM. * * @param mixed $stringValue String Value to check + * Or can be an array of values * - * @return string + * @return array|string + * If an array of values is passed as the argument, then the returned result will also be an array + * with the same dimensions */ public static function spaces($stringValue = '') { + if (is_array($stringValue)) { + return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $stringValue); + } + $stringValue = Helpers::extractString($stringValue); return trim(preg_replace('/ +/', ' ', trim("$stringValue", ' ')) ?? '', ' '); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Token/Stack.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Token/Stack.php index 941e1ad7649..26ffc011c63 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Token/Stack.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Token/Stack.php @@ -3,9 +3,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Token; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Engine\BranchPruner; class Stack { + /** + * @var BranchPruner + */ + private $branchPruner; + /** * The parser stack for formulae. * @@ -20,12 +26,15 @@ class Stack */ private $count = 0; + public function __construct(BranchPruner $branchPruner) + { + $this->branchPruner = $branchPruner; + } + /** * Return the number of entries on the stack. - * - * @return int */ - public function count() + public function count(): int { return $this->count; } @@ -33,25 +42,11 @@ class Stack /** * Push a new entry onto the 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, - $storeKey = null, - $onlyIf = null, - $onlyIfNot = null - ): void { - $stackItem = $this->getStackItem($type, $value, $reference, $storeKey, $onlyIf, $onlyIfNot); - + public function push(string $type, $value, ?string $reference = null): void + { + $stackItem = $this->getStackItem($type, $value, $reference); $this->stack[$this->count++] = $stackItem; if ($type == 'Function') { @@ -62,29 +57,37 @@ class Stack } } - public function getStackItem( - $type, - $value, - $reference = null, - $storeKey = null, - $onlyIf = null, - $onlyIfNot = null - ) { + public function pushStackItem(array $stackItem): void + { + $this->stack[$this->count++] = $stackItem; + } + + /** + * @param mixed $value + */ + public function getStackItem(string $type, $value, ?string $reference = null): array + { $stackItem = [ 'type' => $type, 'value' => $value, 'reference' => $reference, ]; - if (isset($storeKey)) { + // will store the result under this alias + $storeKey = $this->branchPruner->currentCondition(); + if (isset($storeKey) || $reference === 'NULL') { $stackItem['storeKey'] = $storeKey; } - if (isset($onlyIf)) { + // will only run computation if the matching store key is true + $onlyIf = $this->branchPruner->currentOnlyIf(); + if (isset($onlyIf) || $reference === 'NULL') { $stackItem['onlyIf'] = $onlyIf; } - if (isset($onlyIfNot)) { + // will only run computation if the matching store key is false + $onlyIfNot = $this->branchPruner->currentOnlyIfNot(); + if (isset($onlyIfNot) || $reference === 'NULL') { $stackItem['onlyIfNot'] = $onlyIfNot; } @@ -93,10 +96,8 @@ class Stack /** * Pop the last entry from the stack. - * - * @return mixed */ - public function pop() + public function pop(): ?array { if ($this->count > 0) { return $this->stack[--$this->count]; @@ -107,12 +108,8 @@ class Stack /** * Return an entry from the stack without removing it. - * - * @param int $n number indicating how far back in the stack we want to look - * - * @return mixed */ - public function last($n = 1) + public function last(int $n = 1): ?array { if ($this->count - $n < 0) { return null; @@ -129,21 +126,4 @@ 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/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php index 05e04bf912c..697d3a6143a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/Web/Service.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Web; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Settings; use Psr\Http\Client\ClientExceptionInterface; @@ -22,11 +22,11 @@ class Service { $url = trim($url); if (strlen($url) > 2048) { - return Functions::VALUE(); // Invalid URL length + return ExcelError::VALUE(); // Invalid URL length } if (!preg_match('/^http[s]?:\/\//', $url)) { - return Functions::VALUE(); // Invalid protocol + return ExcelError::VALUE(); // Invalid protocol } // Get results from the the webservice @@ -37,16 +37,16 @@ class Service try { $response = $client->sendRequest($request); } catch (ClientExceptionInterface $e) { - return Functions::VALUE(); // cURL error + return ExcelError::VALUE(); // cURL error } if ($response->getStatusCode() != 200) { - return Functions::VALUE(); // cURL error + return ExcelError::VALUE(); // cURL error } $output = $response->getBody()->getContents(); if (strlen($output) > 32767) { - return Functions::VALUE(); // Output not a string or too long + return ExcelError::VALUE(); // Output not a string or too long } return $output; @@ -67,7 +67,7 @@ class Service public static function urlEncode($text) { if (!is_string($text)) { - return Functions::VALUE(); + return ExcelError::VALUE(); } return str_replace('+', '%20', urlencode($text)); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/functionlist.txt b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/functionlist.txt deleted file mode 100644 index 270715cd094..00000000000 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/functionlist.txt +++ /dev/null @@ -1,400 +0,0 @@ -ABS -ACCRINT -ACCRINTM -ACOS -ACOSH -ACOT -ACOTH -ADDRESS -AMORDEGRC -AMORLINC -AND -ARABIC -AREAS -ASC -ASIN -ASINH -ATAN -ATAN2 -ATANH -AVEDEV -AVERAGE -AVERAGEA -AVERAGEIF -AVERAGEIFS -BAHTTEXT -BASE -BESSELI -BESSELJ -BESSELK -BESSELY -BETADIST -BETAINV -BIN2DEC -BIN2HEX -BIN2OCT -BINOMDIST -BITAND -BITLSHIFT -BITOR -BITRSHIFT -BITXOR -CEILING -CEILING.MATH -CEILING.PRECISE -CELL -CHAR -CHIDIST -CHIINV -CHITEST -CHOOSE -CLEAN -CODE -COLUMN -COLUMNS -COMBIN -COMBINA -COMPLEX -CONCAT -CONCATENATE -CONFIDENCE -CONVERT -CORREL -COS -COSH -COT -COTH -COUNT -COUNTA -COUNTBLANK -COUNTIF -COUNTIFS -COUPDAYBS -COUPDAYBS -COUPDAYSNC -COUPNCD -COUPNUM -COUPPCD -COVAR -CRITBINOM -CSC -CSCH -CUBEKPIMEMBER -CUBEMEMBER -CUBEMEMBERPROPERTY -CUBERANKEDMEMBER -CUBESET -CUBESETCOUNT -CUBEVALUE -CUMIPMT -CUMPRINC -DATE -DATEDIF -DATEVALUE -DAVERAGE -DAY -DAYS -DAYS360 -DB -DCOUNT -DCOUNTA -DDB -DEC2BIN -DEC2HEX -DEC2OCT -DEGREES -DELTA -DEVSQ -DGET -DISC -DMAX -DMIN -DOLLAR -DOLLARDE -DOLLARFR -DPRODUCT -DSTDEV -DSTDEVP -DSUM -DURATION -DVAR -DVARP -EDATE -EFFECT -EOMONTH -ERF -ERF.PRECISE -ERFC -ERFC.PRECISE -ERROR.TYPE -EVEN -EXACT -EXP -EXPONDIST -FACT -FACTDOUBLE -FALSE -FDIST -FIND -FINDB -FINV -FISHER -FISHERINV -FIXED -FLOOR -FLOOR.MATH -FLOOR.PRECISE -FORECAST -FREQUENCY -FTEST -FV -FVSCHEDULE -GAMAMDIST -GAMMAINV -GAMMALN -GCD -GEOMEAN -GESTEP -GETPIVOTDATA -GROWTH -HARMEAN -HEX2BIN -HEX2OCT -HLOOKUP -HOUR -HYPERLINK -HYPGEOMDIST -IF -IFERROR -IFS -IMABS -IMAGINARY -IMARGUMENT -IMCONJUGATE -IMCOS -IMCOSH -IMCOT -IMCSC -IMCSCH -IMEXP -IMLN -IMLOG10 -IMLOG2 -IMPOWER -IMPRODUCT -IMREAL -IMSEC -IMSECH -IMSIN -IMSINH -IMSQRT -IMSUB -IMSUM -IMTAN -INDEX -INDIRECT -INFO -INT -INTERCEPT -INTRATE -IPMT -IRR -ISBLANK -ISERR -ISERROR -ISEVEN -ISLOGICAL -ISNA -ISNONTEXT -ISNUMBER -ISODD -ISOWEEKNUM -ISPMT -ISREF -ISTEXT -JIS -KURT -LARGE -LCM -LEFT -LEFTB -LEN -LENB -LINEST -LN -LOG -LOG10 -LOGEST -LOGINV -LOGNORMDIST -LOOKUP -LOWER -MATCH -MAX -MAXA -MAXIFS -MDETERM -MDURATION -MEDIAN -MID -MIDB -MIN -MINA -MINIFS -MINUTE -MINVERSE -MIRR -MMULT -MOD -MODE -MONTH -MROUND -MULTINOMIAL -MUNIT -N -NA -NEGBINOMDIST -NETWORKDAYS -NOMINAL -NORMDIST -NORMINV -NORMSDIST -NORMSINV -NOT -NOW -NPER -NPV -NUMBERVALUE -OCT2BIN -OCT2DEC -OCT2HEX -ODD -ODDFPRICE -ODDFYIELD -ODDLPRICE -ODDLYIELD -OFFSET -OR -PDURATION -PEARSON -PERCENTILE -PERCENTRANK -PERMUT -PERMUTATIONA -PHONETIC -PI -PMT -POISSON -POWER -PPMT -PRICE -PRICEDISC -PRICEMAT -PROB -PRODUCT -PROPER -PV -QUARTILE -QUOTIENT -RADIANS -RAND -RANDBETWEEN -RANK -RATE -RECEIVED -REPLACE -REPLACEB -REPT -RIGHT -RIGHTB -ROMAN -ROUND -ROUNDDOWN -ROUNDUP -ROW -ROWS -RRI -RSQ -RTD -SEARCH -SEARCHB -SEC -SECH -SECOND -SERIESSUM -SHEET -SHEETS -SIGN -SIN -SINH -SKEW -SLN -SLOPE -SMALL -SQRT -SQRTPI -STANDARDIZE -STDEV -STDEV.A -STDEV.P -STDEVA -STDEVP -STDEVPA -STEYX -SUBSTITUTE -SUBTOTAL -SUM -SUMIF -SUMIFS -SUMPRODUCT -SUMSQ -SUMX2MY2 -SUMX2PY2 -SUMXMY2 -SWITCH -SYD -T -TAN -TANH -TBILLEQ -TBILLPRICE -TBILLYIELD -TDIST -TEXT -TEXTJOIN -TIME -TIMEVALUE -TINV -TODAY -TRANSPOSE -TREND -TRIM -TRIMMEAN -TRUE -TRUNC -TTEST -TYPE -UNICHAR -UNIORD -UPPER -USDOLLAR -VALUE -VAR -VARA -VARP -VARPA -VDB -VLOOKUP -WEEKDAY -WEEKNUM -WEIBULL -WORKDAY -XIRR -XNPV -XOR -YEAR -YEARFRAC -YIELD -YIELDDISC -YIELDMAT -ZTEST diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/Translations.xlsx index 518176ab9fb862b37b8d1e7340dfa8341b5abfd3..7b9fb0dddfff921e9831507af3b0dfd53a8ee10a 100644 GIT binary patch delta 95940 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

$Rw9to^d`55cm3Sz|ur*DG>* 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∨TL6_{8Hsjuchy7+nHb#O-Q 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 diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/da/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/da/functions index 6260760bc74..03b68c9bd61 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/da/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/da/functions @@ -245,6 +245,7 @@ ROWS = RÆKKER RTD = RTD TRANSPOSE = TRANSPONER VLOOKUP = LOPSLAG +*RC = RK ## ## Matematiske og trigonometriske funktioner (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/de/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/de/functions index 331232f7e59..d49fc5f1b0e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/de/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/de/functions @@ -243,6 +243,7 @@ ROWS = ZEILEN RTD = RTD TRANSPOSE = MTRANS VLOOKUP = SVERWEIS +*RC = ZS ## ## Mathematische und trigonometrische Funktionen (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/es/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/es/functions index 1f9f2891e6d..88012aa1283 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/es/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/es/functions @@ -245,6 +245,7 @@ ROWS = FILAS RTD = RDTR TRANSPOSE = TRANSPONER VLOOKUP = BUSCARV +*RC = FC ## ## Funciones matemáticas y trigonométricas (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fi/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fi/functions index 33068d93e9f..18f7c8c8b43 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fi/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fi/functions @@ -245,6 +245,7 @@ ROWS = RIVIT RTD = RTD TRANSPOSE = TRANSPONOI VLOOKUP = PHAKU +*RC = RS ## ## Matemaattiset ja trigonometriset funktiot (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fr/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fr/functions index 78b603e9cfb..621cb0db3bc 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fr/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/fr/functions @@ -240,6 +240,7 @@ ROWS = LIGNES RTD = RTD TRANSPOSE = TRANSPOSE VLOOKUP = RECHERCHEV +*RC = LC ## ## Fonctions mathématiques et trigonométriques (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/hu/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/hu/functions index 46b30127146..4a375ea2d05 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/hu/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/hu/functions @@ -245,6 +245,7 @@ ROWS = SOROK RTD = VIA TRANSPOSE = TRANSZPONÁLÁS VLOOKUP = FKERES +*RC = SO ## ## Matematikai és trigonometrikus függvények (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nb/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nb/functions index b0a0f9492ae..d352e1f4186 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nb/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nb/functions @@ -245,6 +245,7 @@ ROWS = RADER RTD = RTD TRANSPOSE = TRANSPONER VLOOKUP = FINN.RAD +*RC = RK ## ## Matematikk- og trigonometrifunksjoner (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions index 0e4f1597db0..ce0b30cc8da 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/nl/functions @@ -244,6 +244,7 @@ ROWS = RIJEN RTD = RTG TRANSPOSE = TRANSPONEREN VLOOKUP = VERT.ZOEKEN +*RC = RK ## ## Wiskundige en trigonometrische functies (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/br/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/br/functions index feba30d9a94..5781b0c7050 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/br/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/br/functions @@ -242,6 +242,7 @@ ROWS = LINS RTD = RTD TRANSPOSE = TRANSPOR VLOOKUP = PROCV +*RC = LC ## ## Funções matemáticas e trigonométricas (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/functions index 8a94d82606f..70a3bb0c7e5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/pt/functions @@ -245,6 +245,7 @@ ROWS = LINS RTD = RTD TRANSPOSE = TRANSPOR VLOOKUP = PROCV +*RC = LC ## ## Funções matemáticas e trigonométricas (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/ru/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/ru/functions index 7f9ce783287..9f05d5afa54 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/ru/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/ru/functions @@ -177,6 +177,7 @@ SYD = АСЧ TBILLEQ = РАВНОКЧЕК TBILLPRICE = ЦЕНАКЧЕК TBILLYIELD = ДОХОДКЧЕК +USDOLLAR = ДОЛЛСША VDB = ПУО XIRR = ЧИСТВНДОХ XNPV = ЧИСТНЗ @@ -231,6 +232,7 @@ AREAS = ОБЛАСТИ CHOOSE = ВЫБОР COLUMN = СТОЛБЕЦ COLUMNS = ЧИСЛСТОЛБ +FILTER = ФИЛЬТР FORMULATEXT = Ф.ТЕКСТ GETPIVOTDATA = ПОЛУЧИТЬ.ДАННЫЕ.СВОДНОЙ.ТАБЛИЦЫ HLOOKUP = ГПР @@ -243,8 +245,13 @@ OFFSET = СМЕЩ ROW = СТРОКА ROWS = ЧСТРОК RTD = ДРВ +SORT = СОРТ +SORTBY = СОРТПО TRANSPOSE = ТРАНСП +UNIQUE = УНИК VLOOKUP = ВПР +XLOOKUP = ПРОСМОТРX +XMATCH = ПОИСКПОЗX ## ## Математические и тригонометрические функции (Math & Trig Functions) @@ -302,6 +309,7 @@ PRODUCT = ПРОИЗВЕД QUOTIENT = ЧАСТНОЕ RADIANS = РАДИАНЫ RAND = СЛЧИС +RANDARRAY = СЛУЧМАССИВ RANDBETWEEN = СЛУЧМЕЖДУ ROMAN = РИМСКОЕ ROUND = ОКРУГЛ @@ -312,6 +320,7 @@ ROUNDUP = ОКРУГЛВВЕРХ SEC = SEC SECH = SECH SERIESSUM = РЯД.СУММ +SEQUENCE = ПОСЛЕДОВ SIGN = ЗНАК SIN = SIN SINH = SINH @@ -447,27 +456,36 @@ Z.TEST = Z.ТЕСТ ## ## Текстовые функции (Text Functions) ## +ARRAYTOTEXT = МАССИВВТЕКСТ BAHTTEXT = БАТТЕКСТ CHAR = СИМВОЛ CLEAN = ПЕЧСИМВ CODE = КОДСИМВ CONCAT = СЦЕП +DBCS = БДЦС DOLLAR = РУБЛЬ EXACT = СОВПАД FIND = НАЙТИ +FINDB = НАЙТИБ FIXED = ФИКСИРОВАННЫЙ -ISTHAIDIGIT = TAYRAKAMIYSA +ISTHAIDIGIT = ЕТАЙЦИФРЫ LEFT = ЛЕВСИМВ +LEFTB = ЛЕВБ LEN = ДЛСТР +LENB = ДЛИНБ LOWER = СТРОЧН MID = ПСТР +MIDB = ПСТРБ NUMBERSTRING = СТРОКАЧИСЕЛ NUMBERVALUE = ЧЗНАЧ PROPER = ПРОПНАЧ REPLACE = ЗАМЕНИТЬ +REPLACEB = ЗАМЕНИТЬБ REPT = ПОВТОР RIGHT = ПРАВСИМВ +RIGHTB = ПРАВБ SEARCH = ПОИСК +SEARCHB = ПОИСКБ SUBSTITUTE = ПОДСТАВИТЬ T = Т TEXT = ТЕКСТ @@ -481,6 +499,7 @@ UNICHAR = ЮНИСИМВ UNICODE = UNICODE UPPER = ПРОПИСН VALUE = ЗНАЧЕН +VALUETOTEXT = ЗНАЧЕНИЕВТЕКСТ ## ## Веб-функции (Web Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/sv/functions b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/sv/functions index 2531b4c1dcf..491ecfb93a1 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/sv/functions +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Calculation/locale/sv/functions @@ -243,6 +243,7 @@ ROWS = RADER RTD = RTD TRANSPOSE = TRANSPONERA VLOOKUP = LETARAD +*RC = RK ## ## Matematiska och trigonometriska funktioner (Math & Trig Functions) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php index 499d248c28d..a23a78b6584 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -2,23 +2,44 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Exception; class AddressHelper { public const R1C1_COORDINATE_REGEX = '/(R((?:\[-?\d*\])|(?:\d*))?)(C((?:\[-?\d*\])|(?:\d*))?)/i'; + /** @return string[] */ + public static function getRowAndColumnChars() + { + $rowChar = 'R'; + $colChar = 'C'; + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_EXCEL) { + $rowColChars = Calculation::localeFunc('*RC'); + if (mb_strlen($rowColChars) === 2) { + $rowChar = mb_substr($rowColChars, 0, 1); + $colChar = mb_substr($rowColChars, 1, 1); + } + } + + return [$rowChar, $colChar]; + } + /** * Converts an R1C1 format cell address to an A1 format cell address. */ public static function convertToA1( string $address, int $currentRowNumber = 1, - int $currentColumnNumber = 1 + int $currentColumnNumber = 1, + bool $useLocale = true ): string { - $validityCheck = preg_match('/^(R(\[?-?\d*\]?))(C(\[?-?\d*\]?))$/i', $address, $cellReference); + [$rowChar, $colChar] = $useLocale ? self::getRowAndColumnChars() : ['R', 'C']; + $regex = '/^(' . $rowChar . '(\[?[-+]?\d*\]?))(' . $colChar . '(\[?[-+]?\d*\]?))$/i'; + $validityCheck = preg_match($regex, $address, $cellReference); - if ($validityCheck === 0) { + if (empty($validityCheck)) { throw new Exception('Invalid R1C1-format Cell Reference'); } @@ -92,7 +113,7 @@ class AddressHelper // Loop through each R1C1 style reference in turn, converting it to its A1 style equivalent, // then modify the formula to use that new reference foreach ($cellReferences as $cellReference) { - $A1CellReference = self::convertToA1($cellReference[0][0], $currentRowNumber, $currentColumnNumber); + $A1CellReference = self::convertToA1($cellReference[0][0], $currentRowNumber, $currentColumnNumber, false); $value = substr_replace($value, $A1CellReference, $cellReference[0][1], strlen($cellReference[0][0])); } } @@ -118,17 +139,17 @@ class AddressHelper throw new Exception('Invalid A1-format Cell Reference'); } - $columnId = Coordinate::columnIndexFromString($cellReference['col_ref']); - if ($cellReference['absolute_col'] === '$') { + if ($cellReference['col'][0] === '$') { // Column must be absolute address $currentColumnNumber = null; } + $columnId = Coordinate::columnIndexFromString(ltrim($cellReference['col'], '$')); - $rowId = (int) $cellReference['row_ref']; - if ($cellReference['absolute_row'] === '$') { + if ($cellReference['row'][0] === '$') { // Row must be absolute address $currentRowNumber = null; } + $rowId = (int) ltrim($cellReference['row'], '$'); if ($currentRowNumber !== null) { if ($rowId === $currentRowNumber) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php new file mode 100644 index 00000000000..54752317694 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/AddressRange.php @@ -0,0 +1,22 @@ +setValueExplicit($days, DataType::TYPE_NUMERIC); @@ -193,6 +195,9 @@ class AdvancedValueBinder extends DefaultValueBinder implements IValueBinder { // Convert value to number [$hours, $minutes, $seconds] = explode(':', $value); + $hours = (int) $hours; + $minutes = (int) $minutes; + $seconds = (int) $seconds; $days = ($hours / 24) + ($minutes / 1440) + ($seconds / 86400); $cell->setValueExplicit($days, DataType::TYPE_NUMERIC); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php index 5a9def375e6..d4d793d789e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Cell.php @@ -3,9 +3,12 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Collection\Cells; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\RichText\RichText; +use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate; +use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\CellStyleAssessor; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -47,14 +50,14 @@ class Cell private $dataType; /** - * Collection of cells. + * The collection of cells that this cell belongs to (i.e. The Cell Collection for the parent Worksheet). * * @var Cells */ private $parent; /** - * Index to cellXf. + * Index to the cellXf reference for the styling of this cell. * * @var int */ @@ -92,9 +95,8 @@ class Cell * Create a new Cell. * * @param mixed $value - * @param string $dataType */ - public function __construct($value, $dataType, Worksheet $worksheet) + public function __construct($value, ?string $dataType, Worksheet $worksheet) { // Initialise cell value $this->value = $value; @@ -108,7 +110,7 @@ class Cell $dataType = DataType::TYPE_STRING; } $this->dataType = $dataType; - } elseif (!self::getValueBinder()->bindValue($this, $value)) { + } elseif (self::getValueBinder()->bindValue($this, $value) === false) { throw new Exception('Value could not be bound to cell.'); } } @@ -164,10 +166,8 @@ class Cell /** * Get cell value with formatting. - * - * @return string */ - public function getFormattedValue() + public function getFormattedValue(): string { return (string) NumberFormat::toFormattedString( $this->getCalculatedValue(), @@ -185,7 +185,7 @@ class Cell * * @return $this */ - public function setValue($value) + public function setValue($value): self { if (!self::getValueBinder()->bindValue($this, $value)) { throw new Exception('Value could not be bound to cell.'); @@ -199,6 +199,11 @@ class Cell * * @param mixed $value Value * @param string $dataType Explicit data type, see DataType::TYPE_* + * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this + * method, then it is your responsibility as an end-user developer to validate that the value and + * the datatype match. + * If you do mismatch value and datatype, then the value you enter may be changed to match the datatype + * that you specify. * * @return Cell */ @@ -207,7 +212,7 @@ class Cell // set the value according to data type switch ($dataType) { case DataType::TYPE_NULL: - $this->value = $value; + $this->value = null; break; case DataType::TYPE_STRING2: @@ -234,6 +239,11 @@ class Cell case DataType::TYPE_BOOL: $this->value = (bool) $value; + break; + case DataType::TYPE_ISO_DATE: + $this->value = SharedDate::convertIsoDate($value); + $dataType = DataType::TYPE_NUMERIC; + break; case DataType::TYPE_ERROR: $this->value = DataType::checkErrorCode($value); @@ -258,9 +268,9 @@ class Cell * * @return mixed */ - public function getCalculatedValue($resetLog = true) + public function getCalculatedValue(bool $resetLog = true) { - if ($this->dataType == DataType::TYPE_FORMULA) { + if ($this->dataType === DataType::TYPE_FORMULA) { try { $index = $this->getWorksheet()->getParent()->getActiveSheetIndex(); $selected = $this->getWorksheet()->getSelectedCells(); @@ -279,7 +289,7 @@ class Cell if (($ex->getMessage() === 'Unable to access External Workbook') && ($this->calculatedValue !== null)) { return $this->calculatedValue; // Fallback for calculations referencing external files. } elseif (preg_match('/[Uu]ndefined (name|offset: 2|array key 2)/', $ex->getMessage()) === 1) { - return \PhpOffice\PhpSpreadsheet\Calculation\Functions::NAME(); + return ExcelError::NAME(); } throw new \PhpOffice\PhpSpreadsheet\Calculation\Exception( @@ -303,10 +313,8 @@ class Cell * Set old calculated value (cached). * * @param mixed $originalValue Value - * - * @return Cell */ - public function setCalculatedValue($originalValue) + public function setCalculatedValue($originalValue): self { if ($originalValue !== null) { $this->calculatedValue = (is_numeric($originalValue)) ? (float) $originalValue : $originalValue; @@ -332,10 +340,8 @@ class Cell /** * Get cell data type. - * - * @return string */ - public function getDataType() + public function getDataType(): string { return $this->dataType; } @@ -344,10 +350,8 @@ class Cell * Set cell data type. * * @param string $dataType see DataType::TYPE_* - * - * @return Cell */ - public function setDataType($dataType) + public function setDataType($dataType): self { if ($dataType == DataType::TYPE_STRING2) { $dataType = DataType::TYPE_STRING; @@ -359,20 +363,16 @@ class Cell /** * Identify if the cell contains a formula. - * - * @return bool */ - public function isFormula() + public function isFormula(): bool { - return $this->dataType == DataType::TYPE_FORMULA; + return $this->dataType === DataType::TYPE_FORMULA && $this->getStyle()->getQuotePrefix() === false; } /** * Does this cell contain Data validation rules? - * - * @return bool */ - public function hasDataValidation() + public function hasDataValidation(): bool { if (!isset($this->parent)) { throw new Exception('Cannot check for data validation when cell is not bound to a worksheet'); @@ -383,10 +383,8 @@ class Cell /** * Get Data validation rules. - * - * @return DataValidation */ - public function getDataValidation() + public function getDataValidation(): DataValidation { if (!isset($this->parent)) { throw new Exception('Cannot get data validation for cell that is not bound to a worksheet'); @@ -411,10 +409,8 @@ class Cell /** * Does this cell contain valid value? - * - * @return bool */ - public function hasValidValue() + public function hasValidValue(): bool { $validator = new DataValidator(); @@ -423,10 +419,8 @@ class Cell /** * Does this cell contain a Hyperlink? - * - * @return bool */ - public function hasHyperlink() + public function hasHyperlink(): bool { if (!isset($this->parent)) { throw new Exception('Cannot check for hyperlink when cell is not bound to a worksheet'); @@ -437,10 +431,8 @@ class Cell /** * Get Hyperlink. - * - * @return Hyperlink */ - public function getHyperlink() + public function getHyperlink(): Hyperlink { if (!isset($this->parent)) { throw new Exception('Cannot get hyperlink for cell that is not bound to a worksheet'); @@ -451,10 +443,8 @@ class Cell /** * Set Hyperlink. - * - * @return Cell */ - public function setHyperlink(?Hyperlink $hyperlink = null) + public function setHyperlink(?Hyperlink $hyperlink = null): self { if (!isset($this->parent)) { throw new Exception('Cannot set hyperlink for cell that is not bound to a worksheet'); @@ -477,10 +467,8 @@ class Cell /** * Get parent worksheet. - * - * @return Worksheet */ - public function getWorksheet() + public function getWorksheet(): Worksheet { try { $worksheet = $this->parent->getParent(); @@ -497,27 +485,22 @@ class Cell /** * Is this cell in a merge range. - * - * @return bool */ - public function isInMergeRange() + public function isInMergeRange(): bool { return (bool) $this->getMergeRange(); } /** * Is this cell the master (top left cell) in a merge range (that holds the actual data value). - * - * @return bool */ - public function isMergeRangeValueCell() + public function isMergeRangeValueCell(): bool { if ($mergeRange = $this->getMergeRange()) { $mergeRange = Coordinate::splitRange($mergeRange); [$startCell] = $mergeRange[0]; - if ($this->getCoordinate() === $startCell) { - return true; - } + + return $this->getCoordinate() === $startCell; } return false; @@ -541,20 +524,34 @@ class Cell /** * Get cell style. - * - * @return Style */ - public function getStyle() + public function getStyle(): Style { return $this->getWorksheet()->getStyle($this->getCoordinate()); } /** - * Re-bind parent. - * - * @return Cell + * Get cell style. */ - public function rebindParent(Worksheet $parent) + public function getAppliedStyle(): Style + { + if ($this->getWorksheet()->conditionalStylesExists($this->getCoordinate()) === false) { + return $this->getStyle(); + } + $range = $this->getWorksheet()->getConditionalRange($this->getCoordinate()); + if ($range === null) { + return $this->getStyle(); + } + + $matcher = new CellStyleAssessor($this, $range); + + return $matcher->matchConditions($this->getWorksheet()->getConditionalStyles($this->getCoordinate())); + } + + /** + * Re-bind parent. + */ + public function rebindParent(Worksheet $parent): self { $this->parent = $parent->getCellCollection(); @@ -565,10 +562,8 @@ class Cell * Is cell in a specific range? * * @param string $range Cell range (e.g. A1:A1) - * - * @return bool */ - public function isInRange($range) + public function isInRange(string $range): bool { [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($range); @@ -589,7 +584,7 @@ class Cell * * @return int Result of comparison (always -1 or 1, never zero!) */ - public static function compareCells(self $a, self $b) + public static function compareCells(self $a, self $b): int { if ($a->getRow() < $b->getRow()) { return -1; @@ -604,10 +599,8 @@ class Cell /** * Get value binder to use. - * - * @return IValueBinder */ - public static function getValueBinder() + public static function getValueBinder(): IValueBinder { if (self::$valueBinder === null) { self::$valueBinder = new DefaultValueBinder(); @@ -630,33 +623,27 @@ class Cell public function __clone() { $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if ((is_object($value)) && ($key != 'parent')) { - $this->$key = clone $value; + foreach ($vars as $propertyName => $propertyValue) { + if ((is_object($propertyValue)) && ($propertyName !== 'parent')) { + $this->$propertyName = clone $propertyValue; } else { - $this->$key = $value; + $this->$propertyName = $propertyValue; } } } /** * Get index to cellXf. - * - * @return int */ - public function getXfIndex() + public function getXfIndex(): int { return $this->xfIndex; } /** * Set index to cellXf. - * - * @param int $indexValue - * - * @return Cell */ - public function setXfIndex($indexValue) + public function setXfIndex(int $indexValue): self { $this->xfIndex = $indexValue; @@ -670,7 +657,7 @@ class Cell * * @return $this */ - public function setFormulaAttributes($attributes) + public function setFormulaAttributes($attributes): self { $this->formulaAttributes = $attributes; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellAddress.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellAddress.php new file mode 100644 index 00000000000..a0c544f2848 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellAddress.php @@ -0,0 +1,166 @@ +cellAddress = str_replace('$', '', $cellAddress); + [$this->columnId, $this->rowId, $this->columnName] = Coordinate::indexesFromString($this->cellAddress); + $this->worksheet = $worksheet; + } + + /** + * @param mixed $columnId + * @param mixed $rowId + */ + private static function validateColumnAndRow($columnId, $rowId): void + { + if (!is_numeric($columnId) || $columnId <= 0 || !is_numeric($rowId) || $rowId <= 0) { + throw new Exception('Row and Column Ids must be positive integer values'); + } + } + + /** + * @param mixed $columnId + * @param mixed $rowId + */ + public static function fromColumnAndRow($columnId, $rowId, ?Worksheet $worksheet = null): self + { + self::validateColumnAndRow($columnId, $rowId); + + /** @phpstan-ignore-next-line */ + return new static(Coordinate::stringFromColumnIndex($columnId) . ((string) $rowId), $worksheet); + } + + public static function fromColumnRowArray(array $array, ?Worksheet $worksheet = null): self + { + [$columnId, $rowId] = $array; + + return static::fromColumnAndRow($columnId, $rowId, $worksheet); + } + + /** + * @param mixed $cellAddress + */ + public static function fromCellAddress($cellAddress, ?Worksheet $worksheet = null): self + { + /** @phpstan-ignore-next-line */ + return new static($cellAddress, $worksheet); + } + + /** + * The returned address string will contain the worksheet name as well, if available, + * (ie. if a Worksheet was provided to the constructor). + * e.g. "'Mark''s Worksheet'!C5". + */ + public function fullCellAddress(): string + { + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$this->cellAddress}"; + } + + return $this->cellAddress; + } + + public function worksheet(): ?Worksheet + { + return $this->worksheet; + } + + /** + * The returned address string will contain just the column/row address, + * (even if a Worksheet was provided to the constructor). + * e.g. "C5". + */ + public function cellAddress(): string + { + return $this->cellAddress; + } + + public function rowId(): int + { + return $this->rowId; + } + + public function columnId(): int + { + return $this->columnId; + } + + public function columnName(): string + { + return $this->columnName; + } + + public function nextRow(int $offset = 1): self + { + $newRowId = $this->rowId + $offset; + if ($newRowId < 1) { + $newRowId = 1; + } + + return static::fromColumnAndRow($this->columnId, $newRowId); + } + + public function previousRow(int $offset = 1): self + { + return $this->nextRow(0 - $offset); + } + + public function nextColumn(int $offset = 1): self + { + $newColumnId = $this->columnId + $offset; + if ($newColumnId < 1) { + $newColumnId = 1; + } + + return static::fromColumnAndRow($newColumnId, $this->rowId); + } + + public function previousColumn(int $offset = 1): self + { + return $this->nextColumn(0 - $offset); + } + + /** + * The returned address string will contain the worksheet name as well, if available, + * (ie. if a Worksheet was provided to the constructor). + * e.g. "'Mark''s Worksheet'!C5". + */ + public function __toString() + { + return $this->fullCellAddress(); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellRange.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellRange.php new file mode 100644 index 00000000000..908a0d076ab --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/CellRange.php @@ -0,0 +1,136 @@ +validateFromTo($from, $to); + } + + private function validateFromTo(CellAddress $from, CellAddress $to): void + { + // Identify actual top-left and bottom-right values (in case we've been given top-right and bottom-left) + $firstColumn = min($from->columnId(), $to->columnId()); + $firstRow = min($from->rowId(), $to->rowId()); + $lastColumn = max($from->columnId(), $to->columnId()); + $lastRow = max($from->rowId(), $to->rowId()); + + $fromWorksheet = $from->worksheet(); + $toWorksheet = $to->worksheet(); + $this->validateWorksheets($fromWorksheet, $toWorksheet); + + $this->from = $this->cellAddressWrapper($firstColumn, $firstRow, $fromWorksheet); + $this->to = $this->cellAddressWrapper($lastColumn, $lastRow, $toWorksheet); + } + + private function validateWorksheets(?Worksheet $fromWorksheet, ?Worksheet $toWorksheet): void + { + if ($fromWorksheet !== null && $toWorksheet !== null) { + // We could simply compare worksheets rather than worksheet titles; but at some point we may introduce + // support for 3d ranges; and at that point we drop this check and let the validation fall through + // to the check for same workbook; but unless we check on titles, this test will also detect if the + // worksheets are in different spreadsheets, and the next check will never execute or throw its + // own exception. + if ($fromWorksheet->getTitle() !== $toWorksheet->getTitle()) { + throw new Exception('3d Cell Ranges are not supported'); + } elseif ($fromWorksheet->getParent() !== $toWorksheet->getParent()) { + throw new Exception('Worksheets must be in the same spreadsheet'); + } + } + } + + private function cellAddressWrapper(int $column, int $row, ?Worksheet $worksheet = null): CellAddress + { + $cellAddress = Coordinate::stringFromColumnIndex($column) . (string) $row; + + return new class ($cellAddress, $worksheet) extends CellAddress { + public function nextRow(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::nextRow($offset); + $this->rowId = $result->rowId; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function previousRow(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::previousRow($offset); + $this->rowId = $result->rowId; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function nextColumn(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::nextColumn($offset); + $this->columnId = $result->columnId; + $this->columnName = $result->columnName; + $this->cellAddress = $result->cellAddress; + + return $this; + } + + public function previousColumn(int $offset = 1): CellAddress + { + /** @var CellAddress $result */ + $result = parent::previousColumn($offset); + $this->columnId = $result->columnId; + $this->columnName = $result->columnName; + $this->cellAddress = $result->cellAddress; + + return $this; + } + }; + } + + public function from(): CellAddress + { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + + return $this->from; + } + + public function to(): CellAddress + { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + + return $this->to; + } + + public function __toString(): string + { + // Re-order from/to in case the cell addresses have been modified + $this->validateFromTo($this->from, $this->to); + + if ($this->from->cellAddress() === $this->to->cellAddress()) { + return "{$this->from->fullCellAddress()}"; + } + + $fromAddress = $this->from->fullCellAddress(); + $toAddress = $this->to->cellAddress(); + + return "{$fromAddress}:{$toAddress}"; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/ColumnRange.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/ColumnRange.php new file mode 100644 index 00000000000..1e521a13197 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/ColumnRange.php @@ -0,0 +1,125 @@ +validateFromTo( + Coordinate::columnIndexFromString($from), + Coordinate::columnIndexFromString($to ?? $from) + ); + $this->worksheet = $worksheet; + } + + public static function fromColumnIndexes(int $from, int $to, ?Worksheet $worksheet = null): self + { + return new self(Coordinate::stringFromColumnIndex($from), Coordinate::stringFromColumnIndex($to), $worksheet); + } + + /** + * @param array $array + */ + public static function fromArray(array $array, ?Worksheet $worksheet = null): self + { + array_walk( + $array, + function (&$column): void { + $column = is_numeric($column) ? Coordinate::stringFromColumnIndex((int) $column) : $column; + } + ); + /** @var string $from */ + /** @var string $to */ + [$from, $to] = $array; + + return new self($from, $to, $worksheet); + } + + private function validateFromTo(int $from, int $to): void + { + // Identify actual top and bottom values (in case we've been given bottom and top) + $this->from = min($from, $to); + $this->to = max($from, $to); + } + + public function columnCount(): int + { + return $this->to - $this->from + 1; + } + + public function shiftDown(int $offset = 1): self + { + $newFrom = $this->from + $offset; + $newFrom = ($newFrom < 1) ? 1 : $newFrom; + + $newTo = $this->to + $offset; + $newTo = ($newTo < 1) ? 1 : $newTo; + + return self::fromColumnIndexes($newFrom, $newTo, $this->worksheet); + } + + public function shiftUp(int $offset = 1): self + { + return $this->shiftDown(0 - $offset); + } + + public function from(): string + { + return Coordinate::stringFromColumnIndex($this->from); + } + + public function to(): string + { + return Coordinate::stringFromColumnIndex($this->to); + } + + public function fromIndex(): int + { + return $this->from; + } + + public function toIndex(): int + { + return $this->to; + } + + public function toCellRange(): CellRange + { + return new CellRange( + CellAddress::fromColumnAndRow($this->from, 1, $this->worksheet), + CellAddress::fromColumnAndRow($this->to, AddressRange::MAX_ROW) + ); + } + + public function __toString(): string + { + $from = $this->from(); + $to = $this->to(); + + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$from}:{$to}"; + } + + return "{$from}:{$to}"; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php index b4b76c57d30..2fca62127d8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/Coordinate.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; @@ -13,7 +14,7 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; */ abstract class Coordinate { - public const A1_COORDINATE_REGEX = '/^(?\$?)(?[A-Z]{1,3})(?\$?)(?\d{1,7})$/i'; + public const A1_COORDINATE_REGEX = '/^(?\$?[A-Z]{1,3})(?\$?\d{1,7})$/i'; /** * Default range variable constant. @@ -32,7 +33,7 @@ abstract class Coordinate public static function coordinateFromString($cellAddress) { if (preg_match(self::A1_COORDINATE_REGEX, $cellAddress, $matches)) { - return [$matches['absolute_col'] . $matches['col_ref'], $matches['absolute_row'] . $matches['row_ref']]; + return [$matches['col'], $matches['row']]; } elseif (self::coordinateIsRange($cellAddress)) { throw new Exception('Cell coordinate string can not be a range of cells'); } elseif ($cellAddress == '') { @@ -47,15 +48,17 @@ abstract class Coordinate * * @param string $coordinates eg: 'A1', '$B$12' * - * @return array{0: int, 1: int} Array containing column index and row index (indexes 0 and 1) + * @return array{0: int, 1: int, 2: string} Array containing column and row index, and column string */ public static function indexesFromString(string $coordinates): array { - [$col, $row] = self::coordinateFromString($coordinates); + [$column, $row] = self::coordinateFromString($coordinates); + $column = ltrim($column, '$'); return [ - self::columnIndexFromString(ltrim($col, '$')), + self::columnIndexFromString($column), (int) ltrim($row, '$'), + $column, ]; } @@ -92,6 +95,7 @@ abstract class Coordinate } // Create absolute coordinate + $cellAddress = "$cellAddress"; if (ctype_digit($cellAddress)) { return $worksheet . '$' . $cellAddress; } elseif (ctype_alpha($cellAddress)) { @@ -147,6 +151,7 @@ abstract class Coordinate $exploded = explode(',', $range); $counter = count($exploded); for ($i = 0; $i < $counter; ++$i) { + // @phpstan-ignore-next-line $exploded[$i] = explode(':', $exploded[$i]); } @@ -179,12 +184,12 @@ abstract class Coordinate /** * Calculate range boundaries. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range coordinates [Start Cell, End Cell] * where Start Cell and End Cell are arrays (Column Number, Row Number) */ - public static function rangeBoundaries($range) + public static function rangeBoundaries(string $range): array { // Ensure $pRange is a valid range if (empty($range)) { @@ -201,6 +206,16 @@ abstract class Coordinate [$rangeA, $rangeB] = explode(':', $range); } + if (is_numeric($rangeA) && is_numeric($rangeB)) { + $rangeA = 'A' . $rangeA; + $rangeB = AddressRange::MAX_COLUMN . $rangeB; + } + + if (ctype_alpha($rangeA) && ctype_alpha($rangeB)) { + $rangeA = $rangeA . '1'; + $rangeB = $rangeB . AddressRange::MAX_ROW; + } + // Calculate range outer borders $rangeStart = self::coordinateFromString($rangeA); $rangeEnd = self::coordinateFromString($rangeB); @@ -215,7 +230,7 @@ abstract class Coordinate /** * Calculate range dimension. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range dimension (width, height) */ @@ -230,29 +245,19 @@ abstract class Coordinate /** * Calculate range boundaries. * - * @param string $range Cell range (e.g. A1:A1) + * @param string $range Cell range, Single Cell, Row/Column Range (e.g. A1:A1, B2, B:C, 2:3) * * @return array Range coordinates [Start Cell, End Cell] * where Start Cell and End Cell are arrays [Column ID, Row Number] */ public static function getRangeBoundaries($range) { - // Ensure $pRange is a valid range - if (empty($range)) { - $range = self::DEFAULT_RANGE; - } + [$rangeA, $rangeB] = self::rangeBoundaries($range); - // Uppercase coordinate - $range = strtoupper($range); - - // Extract range - if (strpos($range, ':') === false) { - $rangeA = $rangeB = $range; - } else { - [$rangeA, $rangeB] = explode(':', $range); - } - - return [self::coordinateFromString($rangeA), self::coordinateFromString($rangeB)]; + return [ + [self::stringFromColumnIndex($rangeA[0]), $rangeA[1]], + [self::stringFromColumnIndex($rangeB[0]), $rangeB[1]], + ]; } /** @@ -272,35 +277,42 @@ abstract class Coordinate if (isset($indexCache[$columnAddress])) { return $indexCache[$columnAddress]; } - // It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array rather than use ord() - // and make it case insensitive to get rid of the strtoupper() as well. Because it's a static, there's no significant - // memory overhead either + // It's surprising how costly the strtoupper() and ord() calls actually are, so we use a lookup array + // rather than use ord() and make it case insensitive to get rid of the strtoupper() as well. + // Because it's a static, there's no significant memory overhead either. static $columnLookup = [ - 'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, 'K' => 11, 'L' => 12, 'M' => 13, - 'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26, - 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, 'k' => 11, 'l' => 12, 'm' => 13, - 'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26, + 'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, + 'K' => 11, 'L' => 12, 'M' => 13, 'N' => 14, 'O' => 15, 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, + 'T' => 20, 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, 'Z' => 26, + 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, + 'k' => 11, 'l' => 12, 'm' => 13, 'n' => 14, 'o' => 15, 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, + 't' => 20, 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, 'z' => 26, ]; - // We also use the language construct isset() rather than the more costly strlen() function to match the length of $columnAddress - // for improved performance + // We also use the language construct isset() rather than the more costly strlen() function to match the + // length of $columnAddress for improved performance if (isset($columnAddress[0])) { if (!isset($columnAddress[1])) { $indexCache[$columnAddress] = $columnLookup[$columnAddress]; return $indexCache[$columnAddress]; } elseif (!isset($columnAddress[2])) { - $indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26 + $columnLookup[$columnAddress[1]]; + $indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 26 + + $columnLookup[$columnAddress[1]]; return $indexCache[$columnAddress]; } elseif (!isset($columnAddress[3])) { - $indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676 + $columnLookup[$columnAddress[1]] * 26 + $columnLookup[$columnAddress[2]]; + $indexCache[$columnAddress] = $columnLookup[$columnAddress[0]] * 676 + + $columnLookup[$columnAddress[1]] * 26 + + $columnLookup[$columnAddress[2]]; return $indexCache[$columnAddress]; } } - throw new Exception('Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty')); + throw new Exception( + 'Column string index can not be ' . ((isset($columnAddress[0])) ? 'longer than 3 characters' : 'empty') + ); } /** @@ -313,14 +325,15 @@ abstract class Coordinate public static function stringFromColumnIndex($columnIndex) { static $indexCache = []; + static $lookupCache = ' ABCDEFGHIJKLMNOPQRSTUVWXYZ'; if (!isset($indexCache[$columnIndex])) { $indexValue = $columnIndex; - $base26 = null; + $base26 = ''; do { $characterValue = ($indexValue % 26) ?: 26; $indexValue = ($indexValue - $characterValue) / 26; - $base26 = chr($characterValue + 64) . ($base26 ?: ''); + $base26 = $lookupCache[$characterValue] . $base26; } while ($indexValue > 0); $indexCache[$columnIndex] = $base26; } @@ -337,6 +350,19 @@ abstract class Coordinate */ public static function extractAllCellReferencesInRange($cellRange): array { + if (substr_count($cellRange, '!') > 1) { + throw new Exception('3-D Range References are not supported'); + } + + [$worksheet, $cellRange] = Worksheet::extractSheetTitle($cellRange, true); + $quoted = ''; + if ($worksheet > '') { + $quoted = Worksheet::nameRequiresQuotes($worksheet) ? "'" : ''; + if (substr($worksheet, 0, 1) === "'" && substr($worksheet, -1, 1) === "'") { + $worksheet = substr($worksheet, 1, -1); + } + $worksheet = str_replace("'", "''", $worksheet); + } [$ranges, $operators] = self::getCellBlocksFromRangeString($cellRange); $cells = []; @@ -351,9 +377,13 @@ abstract class Coordinate } $cellList = array_merge(...$cells); - $cellList = self::sortCellReferenceArray($cellList); - return $cellList; + return array_map( + function ($cellAddress) use ($worksheet, $quoted) { + return ($worksheet !== '') ? "{$quoted}{$worksheet}{$quoted}!{$cellAddress}" : $cellAddress; + }, + self::sortCellReferenceArray($cellList) + ); } private static function processRangeSetOperators(array $operators, array $cells): array @@ -380,9 +410,10 @@ abstract class Coordinate { // Sort the result by column and row $sortKeys = []; - foreach ($cellList as $coord) { - [$column, $row] = sscanf($coord, '%[A-Z]%d'); - $sortKeys[sprintf('%3s%09d', $column, $row)] = $coord; + foreach ($cellList as $coordinate) { + sscanf($coordinate, '%[A-Z]%d', $column, $row); + $key = (--$row * 16384) + self::columnIndexFromString($column); + $sortKeys[$key] = $coordinate; } ksort($sortKeys); @@ -543,7 +574,7 @@ abstract class Coordinate // split range sets on intersection (space) or union (,) operators $tokens = preg_split('/([ ,])/', $rangeString, -1, PREG_SPLIT_DELIM_CAPTURE); - // separate the range sets and the operators into arrays + /** @phpstan-ignore-next-line */ $split = array_chunk($tokens, 2); $ranges = array_column($split, 0); $operators = array_column($split, 1); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php index cee3e1e5eb6..f19984db6a7 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataType.php @@ -16,11 +16,12 @@ class DataType const TYPE_NULL = 'null'; const TYPE_INLINE = 'inlineStr'; const TYPE_ERROR = 'e'; + const TYPE_ISO_DATE = 'd'; /** * List of error codes. * - * @var array + * @var array */ private static $errorCodes = [ '#NULL!' => 0, @@ -30,12 +31,15 @@ class DataType '#NAME?' => 4, '#NUM!' => 5, '#N/A' => 6, + '#CALC!' => 7, ]; + public const MAX_STRING_LENGTH = 32767; + /** * Get list of error codes. * - * @return array + * @return array */ public static function getErrorCodes() { @@ -47,7 +51,7 @@ class DataType * * @param null|RichText|string $textValue Value to sanitize to an Excel string * - * @return null|RichText|string Sanitized value + * @return RichText|string Sanitized value */ public static function checkString($textValue) { @@ -57,7 +61,7 @@ class DataType } // string must never be longer than 32,767 characters, truncate if necessary - $textValue = StringHelper::substring($textValue, 0, 32767); + $textValue = StringHelper::substring((string) $textValue, 0, self::MAX_STRING_LENGTH); // we require that newline is represented as "\n" in core, not as "\r\n" or "\r" $textValue = str_replace(["\r\n", "\r"], "\n", $textValue); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataValidator.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataValidator.php index 430d81b96e4..0e395a7ff99 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataValidator.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/DataValidator.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Cell; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Exception; /** @@ -64,8 +64,11 @@ class DataValidator try { $result = $calculation->calculateFormula($matchFormula, $cell->getCoordinate(), $cell); + while (is_array($result)) { + $result = array_pop($result); + } - return $result !== Functions::NA(); + return $result !== ExcelError::NA(); } catch (Exception $ex) { return false; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/RowRange.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/RowRange.php new file mode 100644 index 00000000000..38e6c1415b6 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Cell/RowRange.php @@ -0,0 +1,93 @@ +validateFromTo($from, $to ?? $from); + $this->worksheet = $worksheet; + } + + public static function fromArray(array $array, ?Worksheet $worksheet = null): self + { + [$from, $to] = $array; + + return new self($from, $to, $worksheet); + } + + private function validateFromTo(int $from, int $to): void + { + // Identify actual top and bottom values (in case we've been given bottom and top) + $this->from = min($from, $to); + $this->to = max($from, $to); + } + + public function from(): int + { + return $this->from; + } + + public function to(): int + { + return $this->to; + } + + public function rowCount(): int + { + return $this->to - $this->from + 1; + } + + public function shiftRight(int $offset = 1): self + { + $newFrom = $this->from + $offset; + $newFrom = ($newFrom < 1) ? 1 : $newFrom; + + $newTo = $this->to + $offset; + $newTo = ($newTo < 1) ? 1 : $newTo; + + return new self($newFrom, $newTo, $this->worksheet); + } + + public function shiftLeft(int $offset = 1): self + { + return $this->shiftRight(0 - $offset); + } + + public function toCellRange(): CellRange + { + return new CellRange( + CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString('A'), $this->from, $this->worksheet), + CellAddress::fromColumnAndRow(Coordinate::columnIndexFromString(AddressRange::MAX_COLUMN), $this->to) + ); + } + + public function __toString(): string + { + if ($this->worksheet !== null) { + $title = str_replace("'", "''", $this->worksheet->getTitle()); + + return "'{$title}'!{$this->from}:{$this->to}"; + } + + return "{$this->from}:{$this->to}"; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/CellReferenceHelper.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/CellReferenceHelper.php new file mode 100644 index 00000000000..0f079d6978a --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/CellReferenceHelper.php @@ -0,0 +1,119 @@ +beforeCellAddress = str_replace('$', '', $beforeCellAddress); + $this->numberOfColumns = $numberOfColumns; + $this->numberOfRows = $numberOfRows; + + // Get coordinate of $beforeCellAddress + [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress); + $this->beforeColumn = (int) Coordinate::columnIndexFromString($beforeColumn); + $this->beforeRow = (int) $beforeRow; + } + + public function beforeCellAddress(): string + { + return $this->beforeCellAddress; + } + + public function refreshRequired(string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): bool + { + return $this->beforeCellAddress !== $beforeCellAddress || + $this->numberOfColumns !== $numberOfColumns || + $this->numberOfRows !== $numberOfRows; + } + + public function updateCellReference(string $cellReference = 'A1', bool $includeAbsoluteReferences = false): string + { + if (Coordinate::coordinateIsRange($cellReference)) { + throw new Exception('Only single cell references may be passed to this method.'); + } + + // Get coordinate of $cellReference + [$newColumn, $newRow] = Coordinate::coordinateFromString($cellReference); + $newColumnIndex = (int) Coordinate::columnIndexFromString(str_replace('$', '', $newColumn)); + $newRowIndex = (int) str_replace('$', '', $newRow); + + $absoluteColumn = $newColumn[0] === '$' ? '$' : ''; + $absoluteRow = $newRow[0] === '$' ? '$' : ''; + // Verify which parts should be updated + if ($includeAbsoluteReferences === false) { + $updateColumn = (($absoluteColumn !== '$') && $newColumnIndex >= $this->beforeColumn); + $updateRow = (($absoluteRow !== '$') && $newRowIndex >= $this->beforeRow); + } else { + $updateColumn = ($newColumnIndex >= $this->beforeColumn); + $updateRow = ($newRowIndex >= $this->beforeRow); + } + + // Create new column reference + if ($updateColumn) { + $newColumn = ($includeAbsoluteReferences === false) + ? Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns) + : $absoluteColumn . Coordinate::stringFromColumnIndex($newColumnIndex + $this->numberOfColumns); + } + + // Create new row reference + if ($updateRow) { + $newRow = ($includeAbsoluteReferences === false) + ? $newRowIndex + $this->numberOfRows + : $absoluteRow . (string) ($newRowIndex + $this->numberOfRows); + } + + // Return new reference + return "{$newColumn}{$newRow}"; + } + + public function cellAddressInDeleteRange(string $cellAddress): bool + { + [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress); + $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); + // Is cell within the range of rows/columns if we're deleting + if ( + $this->numberOfRows < 0 && + ($cellRow >= ($this->beforeRow + $this->numberOfRows)) && + ($cellRow < $this->beforeRow) + ) { + return true; + } elseif ( + $this->numberOfColumns < 0 && + ($cellColumnIndex >= ($this->beforeColumn + $this->numberOfColumns)) && + ($cellColumnIndex < $this->beforeColumn) + ) { + return true; + } + + return false; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php index eeed326db87..9ebb081fa65 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Axis.php @@ -10,6 +10,34 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ class Axis extends Properties { + const AXIS_TYPE_CATEGORY = 'catAx'; + const AXIS_TYPE_DATE = 'dateAx'; + const AXIS_TYPE_VALUE = 'valAx'; + + const TIME_UNIT_DAYS = 'days'; + const TIME_UNIT_MONTHS = 'months'; + const TIME_UNIT_YEARS = 'years'; + + public function __construct() + { + parent::__construct(); + $this->fillColor = new ChartColor(); + } + + /** + * Chart Major Gridlines as. + * + * @var ?GridLines + */ + private $majorGridlines; + + /** + * Chart Minor Gridlines as. + * + * @var ?GridLines + */ + private $minorGridlines; + /** * Axis Number. * @@ -18,8 +46,12 @@ class Axis extends Properties private $axisNumber = [ 'format' => self::FORMAT_CODE_GENERAL, 'source_linked' => 1, + 'numeric' => null, ]; + /** @var string */ + private $axisType = ''; + /** * Axis Options. * @@ -36,99 +68,24 @@ class Axis extends Properties 'axis_labels' => self::AXIS_LABELS_NEXT_TO, 'horizontal_crosses' => self::HORIZONTAL_CROSSES_AUTOZERO, 'horizontal_crosses_value' => null, + 'textRotation' => null, + 'hidden' => null, + 'majorTimeUnit' => self::TIME_UNIT_YEARS, + 'minorTimeUnit' => self::TIME_UNIT_MONTHS, + 'baseTimeUnit' => self::TIME_UNIT_DAYS, ]; /** * Fill Properties. * - * @var mixed[] + * @var ChartColor */ - private $fillProperties = [ - 'type' => self::EXCEL_COLOR_TYPE_ARGB, - 'value' => null, - 'alpha' => 0, - ]; + private $fillColor; - /** - * Line Properties. - * - * @var mixed[] - */ - private $lineProperties = [ - 'type' => self::EXCEL_COLOR_TYPE_ARGB, - 'value' => null, - 'alpha' => 0, - ]; - - /** - * Line Style Properties. - * - * @var mixed[] - */ - private $lineStyleProperties = [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ]; - - /** - * Shadow Properties. - * - * @var mixed[] - */ - private $shadowProperties = [ - 'presets' => self::SHADOW_PRESETS_NOSHADOW, - 'effect' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - 'size' => [ - 'sx' => null, - 'sy' => null, - 'kx' => null, - ], - 'blur' => null, - 'direction' => null, - 'distance' => null, - 'algn' => null, - 'rotWithShape' => null, - ]; - - /** - * Glow Properties. - * - * @var mixed[] - */ - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - /** - * Soft Edge Properties. - * - * @var mixed[] - */ - private $softEdges = [ - 'size' => null, + private const NUMERIC_FORMAT = [ + Properties::FORMAT_CODE_NUMBER, + Properties::FORMAT_CODE_DATE, + Properties::FORMAT_CODE_DATE_ISO8601, ]; /** @@ -136,10 +93,16 @@ class Axis extends Properties * * @param mixed $format_code */ - public function setAxisNumberProperties($format_code): void + public function setAxisNumberProperties($format_code, ?bool $numeric = null, int $sourceLinked = 0): void { - $this->axisNumber['format'] = (string) $format_code; - $this->axisNumber['source_linked'] = 0; + $format = (string) $format_code; + $this->axisNumber['format'] = $format; + $this->axisNumber['source_linked'] = $sourceLinked; + if (is_bool($numeric)) { + $this->axisNumber['numeric'] = $numeric; + } elseif (in_array($format, self::NUMERIC_FORMAT, true)) { + $this->axisNumber['numeric'] = true; + } } /** @@ -162,33 +125,53 @@ class Axis extends Properties return (string) $this->axisNumber['source_linked']; } + public function getAxisIsNumericFormat(): bool + { + return $this->axisType === self::AXIS_TYPE_DATE || (bool) $this->axisNumber['numeric']; + } + + public function setAxisOption(string $key, ?string $value): void + { + if ($value !== null && $value !== '') { + $this->axisOptions[$key] = $value; + } + } + /** * Set Axis Options Properties. - * - * @param string $axisLabels - * @param string $horizontalCrossesValue - * @param string $horizontalCrosses - * @param string $axisOrientation - * @param string $majorTmt - * @param string $minorTmt - * @param string $minimum - * @param string $maximum - * @param string $majorUnit - * @param string $minorUnit */ - public function setAxisOptionsProperties($axisLabels, $horizontalCrossesValue = null, $horizontalCrosses = null, $axisOrientation = null, $majorTmt = null, $minorTmt = null, $minimum = null, $maximum = null, $majorUnit = null, $minorUnit = null): void - { - $this->axisOptions['axis_labels'] = (string) $axisLabels; - ($horizontalCrossesValue !== null) ? $this->axisOptions['horizontal_crosses_value'] = (string) $horizontalCrossesValue : null; - ($horizontalCrosses !== null) ? $this->axisOptions['horizontal_crosses'] = (string) $horizontalCrosses : null; - ($axisOrientation !== null) ? $this->axisOptions['orientation'] = (string) $axisOrientation : null; - ($majorTmt !== null) ? $this->axisOptions['major_tick_mark'] = (string) $majorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minorTmt !== null) ? $this->axisOptions['minor_tick_mark'] = (string) $minorTmt : null; - ($minimum !== null) ? $this->axisOptions['minimum'] = (string) $minimum : null; - ($maximum !== null) ? $this->axisOptions['maximum'] = (string) $maximum : null; - ($majorUnit !== null) ? $this->axisOptions['major_unit'] = (string) $majorUnit : null; - ($minorUnit !== null) ? $this->axisOptions['minor_unit'] = (string) $minorUnit : null; + public function setAxisOptionsProperties( + string $axisLabels, + ?string $horizontalCrossesValue = null, + ?string $horizontalCrosses = null, + ?string $axisOrientation = null, + ?string $majorTmt = null, + ?string $minorTmt = null, + ?string $minimum = null, + ?string $maximum = null, + ?string $majorUnit = null, + ?string $minorUnit = null, + ?string $textRotation = null, + ?string $hidden = null, + ?string $baseTimeUnit = null, + ?string $majorTimeUnit = null, + ?string $minorTimeUnit = null + ): void { + $this->axisOptions['axis_labels'] = $axisLabels; + $this->setAxisOption('horizontal_crosses_value', $horizontalCrossesValue); + $this->setAxisOption('horizontal_crosses', $horizontalCrosses); + $this->setAxisOption('orientation', $axisOrientation); + $this->setAxisOption('major_tick_mark', $majorTmt); + $this->setAxisOption('minor_tick_mark', $minorTmt); + $this->setAxisOption('minimum', $minimum); + $this->setAxisOption('maximum', $maximum); + $this->setAxisOption('major_unit', $majorUnit); + $this->setAxisOption('minor_unit', $minorUnit); + $this->setAxisOption('textRotation', $textRotation); + $this->setAxisOption('hidden', $hidden); + $this->setAxisOption('baseTimeUnit', $baseTimeUnit); + $this->setAxisOption('majorTimeUnit', $majorTimeUnit); + $this->setAxisOption('minorTimeUnit', $minorTimeUnit); } /** @@ -196,7 +179,7 @@ class Axis extends Properties * * @param string $property * - * @return string + * @return ?string */ public function getAxisOptionsProperty($property) { @@ -213,28 +196,32 @@ class Axis extends Properties $this->axisOptions['orientation'] = (string) $orientation; } - /** - * Set Fill Property. - * - * @param string $color - * @param int $alpha - * @param string $AlphaType - */ - public function setFillParameters($color, $alpha = 0, $AlphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function getAxisType(): string { - $this->fillProperties = $this->setColorProperties($color, $alpha, $AlphaType); + return $this->axisType; + } + + public function setAxisType(string $type): self + { + if ($type === self::AXIS_TYPE_CATEGORY || $type === self::AXIS_TYPE_VALUE || $type === self::AXIS_TYPE_DATE) { + $this->axisType = $type; + } else { + $this->axisType = ''; + } + + return $this; } /** - * Set Line Property. + * Set Fill Property. * - * @param string $color - * @param int $alpha - * @param string $alphaType + * @param ?string $color + * @param ?int $alpha + * @param ?string $AlphaType */ - public function setLineParameters($color, $alpha = 0, $alphaType = self::EXCEL_COLOR_TYPE_ARGB): void + public function setFillParameters($color, $alpha = null, $AlphaType = ChartColor::EXCEL_COLOR_TYPE_RGB): void { - $this->lineProperties = $this->setColorProperties($color, $alpha, $alphaType); + $this->fillColor->setColorProperties($color, $alpha, $AlphaType); } /** @@ -246,309 +233,67 @@ class Axis extends Properties */ public function getFillProperty($property) { - return $this->fillProperties[$property]; + return (string) $this->fillColor->getColorProperty($property); + } + + public function getFillColorObject(): ChartColor + { + return $this->fillColor; } /** - * Get Line Property. + * Get Line Color Property. * - * @param string $property + * @Deprecated 1.24.0 * - * @return string + * @See Properties::getLineColorProperty() + * Use the getLineColor property in the Properties class instead + * + * @param string $propertyName + * + * @return null|int|string */ - public function getLineProperty($property) + public function getLineProperty($propertyName) { - return $this->lineProperties[$property]; + return $this->getLineColorProperty($propertyName); } - /** - * Set Line Style Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - ($lineWidth !== null) ? $this->lineStyleProperties['width'] = $this->getExcelPointsWidth((float) $lineWidth) : null; - ($compoundType !== null) ? $this->lineStyleProperties['compound'] = (string) $compoundType : null; - ($dashType !== null) ? $this->lineStyleProperties['dash'] = (string) $dashType : null; - ($capType !== null) ? $this->lineStyleProperties['cap'] = (string) $capType : null; - ($joinType !== null) ? $this->lineStyleProperties['join'] = (string) $joinType : null; - ($headArrowType !== null) ? $this->lineStyleProperties['arrow']['head']['type'] = (string) $headArrowType : null; - ($headArrowSize !== null) ? $this->lineStyleProperties['arrow']['head']['size'] = (string) $headArrowSize : null; - ($endArrowType !== null) ? $this->lineStyleProperties['arrow']['end']['type'] = (string) $endArrowType : null; - ($endArrowSize !== null) ? $this->lineStyleProperties['arrow']['end']['size'] = (string) $endArrowSize : null; - } + /** @var string */ + private $crossBetween = ''; // 'between' or 'midCat' might be better - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) + public function setCrossBetween(string $crossBetween): self { - return $this->getArrayElementsValue($this->lineStyleProperties, $elements); - } - - /** - * Get Line Style Arrow Excel Width. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowWidth($arrow) - { - return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'w'); - } - - /** - * Get Line Style Arrow Excel Length. - * - * @param string $arrow - * - * @return string - */ - public function getLineStyleArrowLength($arrow) - { - return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrow]['size'], 'len'); - } - - /** - * Set Shadow Properties. - * - * @param int $shadowPresets - * @param string $colorValue - * @param string $colorType - * @param string $colorAlpha - * @param float $blur - * @param int $angle - * @param float $distance - */ - public function setShadowProperties($shadowPresets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void - { - $this->setShadowPresetsProperties((int) $shadowPresets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - $colorAlpha ?? (int) $this->shadowProperties['color']['alpha'], - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur($blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); - } - - /** - * Set Shadow Color. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) - { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + $this->crossBetween = $crossBetween; return $this; } - /** - * Set Shadow Properties from Mapped Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) + public function getCrossBetween(): string { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if ($reference === null) { - $reference = &$this->shadowProperties[$property_key]; - } else { - $reference = &$reference[$property_key]; - } - $this->setShadowPropertiesMapValues($property_val, $reference); - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } + return $this->crossBetween; + } + + public function getMajorGridlines(): ?GridLines + { + return $this->majorGridlines; + } + + public function getMinorGridlines(): ?GridLines + { + return $this->minorGridlines; + } + + public function setMajorGridlines(?GridLines $gridlines): self + { + $this->majorGridlines = $gridlines; return $this; } - /** - * Set Shadow Color. - * - * @param string $color - * @param int $alpha - * @param string $alphaType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $alphaType) + public function setMinorGridlines(?GridLines $gridlines): self { - $this->shadowProperties['color'] = $this->setColorProperties($color, $alpha, $alphaType); + $this->minorGridlines = $gridlines; return $this; } - - /** - * Set Shadow Blur. - * - * @param float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = (string) $this->getExcelPointsWidth($blur); - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param int $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if ($angle !== null) { - $this->shadowProperties['direction'] = (string) $this->getExcelPointsAngle($angle); - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = (string) $this->getExcelPointsWidth($distance); - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return null|array|int|string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param string $colorValue - * @param int $colorAlpha - * @param string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this->setGlowSize($size) - ->setGlowColor( - $colorValue ?? $this->glowProperties['color']['value'], - $colorAlpha ?? (int) $this->glowProperties['color']['alpha'], - $colorType ?? $this->glowProperties['color']['type'] - ); - } - - /** - * Get Glow Property. - * - * @param array|string $property - * - * @return string - */ - public function getGlowProperty($property) - { - return $this->getArrayElementsValue($this->glowProperties, $property); - } - - /** - * Set Glow Color. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - if ($size !== null) { - $this->glowProperties['size'] = $this->getExcelPointsWidth($size); - } - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - $this->glowProperties['color'] = $this->setColorProperties($color, $alpha, $colorType); - - return $this; - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdges($size): void - { - if ($size !== null) { - $softEdges['size'] = (string) $this->getExcelPointsWidth($size); - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; - } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php index bed89464ef3..f41d788308f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Chart.php @@ -17,42 +17,42 @@ class Chart /** * Worksheet. * - * @var Worksheet + * @var ?Worksheet */ private $worksheet; /** * Chart Title. * - * @var Title + * @var ?Title */ private $title; /** * Chart Legend. * - * @var Legend + * @var ?Legend */ private $legend; /** * X-Axis Label. * - * @var Title + * @var ?Title */ private $xAxisLabel; /** * Y-Axis Label. * - * @var Title + * @var ?Title */ private $yAxisLabel; /** * Chart Plot Area. * - * @var PlotArea + * @var ?PlotArea */ private $plotArea; @@ -84,20 +84,6 @@ class Chart */ private $xAxis; - /** - * Chart Major Gridlines as. - * - * @var GridLines - */ - private $majorGridlines; - - /** - * Chart Minor Gridlines as. - * - * @var GridLines - */ - private $minorGridlines; - /** * Top-Left Cell Position. * @@ -124,7 +110,7 @@ class Chart * * @var string */ - private $bottomRightCellRef = 'A1'; + private $bottomRightCellRef = ''; /** * Bottom-Right X-Offset. @@ -140,8 +126,33 @@ class Chart */ private $bottomRightYOffset = 10; + /** @var ?int */ + private $rotX; + + /** @var ?int */ + private $rotY; + + /** @var ?int */ + private $rAngAx; + + /** @var ?int */ + private $perspective; + + /** @var bool */ + private $oneCellAnchor = false; + + /** @var bool */ + private $autoTitleDeleted = false; + + /** @var bool */ + private $noFill = false; + + /** @var bool */ + private $roundedCorners = false; + /** * Create a new Chart. + * majorGridlines and minorGridlines are deprecated, moved to Axis. * * @param mixed $name * @param mixed $plotVisibleOnly @@ -157,10 +168,14 @@ class Chart $this->plotArea = $plotArea; $this->plotVisibleOnly = $plotVisibleOnly; $this->displayBlanksAs = $displayBlanksAs; - $this->xAxis = $xAxis; - $this->yAxis = $yAxis; - $this->majorGridlines = $majorGridlines; - $this->minorGridlines = $minorGridlines; + $this->xAxis = $xAxis ?? new Axis(); + $this->yAxis = $yAxis ?? new Axis(); + if ($majorGridlines !== null) { + $this->yAxis->setMajorGridlines($majorGridlines); + } + if ($minorGridlines !== null) { + $this->yAxis->setMinorGridlines($minorGridlines); + } } /** @@ -173,12 +188,17 @@ class Chart return $this->name; } + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + /** * Get Worksheet. - * - * @return Worksheet */ - public function getWorksheet() + public function getWorksheet(): ?Worksheet { return $this->worksheet; } @@ -195,12 +215,7 @@ class Chart return $this; } - /** - * Get Title. - * - * @return Title - */ - public function getTitle() + public function getTitle(): ?Title { return $this->title; } @@ -217,12 +232,7 @@ class Chart return $this; } - /** - * Get Legend. - * - * @return Legend - */ - public function getLegend() + public function getLegend(): ?Legend { return $this->legend; } @@ -239,12 +249,7 @@ class Chart return $this; } - /** - * Get X-Axis Label. - * - * @return Title - */ - public function getXAxisLabel() + public function getXAxisLabel(): ?Title { return $this->xAxisLabel; } @@ -261,12 +266,7 @@ class Chart return $this; } - /** - * Get Y-Axis Label. - * - * @return Title - */ - public function getYAxisLabel() + public function getYAxisLabel(): ?Title { return $this->yAxisLabel; } @@ -283,16 +283,21 @@ class Chart return $this; } - /** - * Get Plot Area. - * - * @return PlotArea - */ - public function getPlotArea() + public function getPlotArea(): ?PlotArea { return $this->plotArea; } + /** + * Set Plot Area. + */ + public function setPlotArea(PlotArea $plotArea): self + { + $this->plotArea = $plotArea; + + return $this; + } + /** * Get Plot Visible Only. * @@ -341,74 +346,72 @@ class Chart return $this; } - /** - * Get yAxis. - * - * @return Axis - */ - public function getChartAxisY() + public function getChartAxisY(): Axis { - if ($this->yAxis !== null) { - return $this->yAxis; - } - - return new Axis(); + return $this->yAxis; } /** - * Get xAxis. - * - * @return Axis + * Set yAxis. */ - public function getChartAxisX() + public function setChartAxisY(?Axis $axis): self { - if ($this->xAxis !== null) { - return $this->xAxis; - } + $this->yAxis = $axis ?? new Axis(); - return new Axis(); + return $this; + } + + public function getChartAxisX(): Axis + { + return $this->xAxis; + } + + /** + * Set xAxis. + */ + public function setChartAxisX(?Axis $axis): self + { + $this->xAxis = $axis ?? new Axis(); + + return $this; } /** * Get Major Gridlines. * - * @return GridLines + * @Deprecated 1.24.0 Use Axis->getMajorGridlines + * + * @codeCoverageIgnore */ - public function getMajorGridlines() + public function getMajorGridlines(): ?GridLines { - if ($this->majorGridlines !== null) { - return $this->majorGridlines; - } - - return new GridLines(); + return $this->yAxis->getMajorGridLines(); } /** * Get Minor Gridlines. * - * @return GridLines + * @Deprecated 1.24.0 Use Axis->getMinorGridlines + * + * @codeCoverageIgnore */ - public function getMinorGridlines() + public function getMinorGridlines(): ?GridLines { - if ($this->minorGridlines !== null) { - return $this->minorGridlines; - } - - return new GridLines(); + return $this->yAxis->getMinorGridLines(); } /** * Set the Top Left position for the chart. * - * @param string $cell + * @param string $cellAddress * @param int $xOffset * @param int $yOffset * * @return $this */ - public function setTopLeftPosition($cell, $xOffset = null, $yOffset = null) + public function setTopLeftPosition($cellAddress, $xOffset = null, $yOffset = null) { - $this->topLeftCellRef = $cell; + $this->topLeftCellRef = $cellAddress; if ($xOffset !== null) { $this->setTopLeftXOffset($xOffset); } @@ -446,13 +449,13 @@ class Chart /** * Set the Top Left cell position for the chart. * - * @param string $cell + * @param string $cellAddress * * @return $this */ - public function setTopLeftCell($cell) + public function setTopLeftCell($cellAddress) { - $this->topLeftCellRef = $cell; + $this->topLeftCellRef = $cellAddress; return $this; } @@ -491,6 +494,11 @@ class Chart ]; } + /** + * @param int $xOffset + * + * @return $this + */ public function setTopLeftXOffset($xOffset) { $this->topLeftXOffset = $xOffset; @@ -498,11 +506,16 @@ class Chart return $this; } - public function getTopLeftXOffset() + public function getTopLeftXOffset(): int { return $this->topLeftXOffset; } + /** + * @param int $yOffset + * + * @return $this + */ public function setTopLeftYOffset($yOffset) { $this->topLeftYOffset = $yOffset; @@ -510,7 +523,7 @@ class Chart return $this; } - public function getTopLeftYOffset() + public function getTopLeftYOffset(): int { return $this->topLeftYOffset; } @@ -518,15 +531,15 @@ class Chart /** * Set the Bottom Right position of the chart. * - * @param string $cell + * @param string $cellAddress * @param int $xOffset * @param int $yOffset * * @return $this */ - public function setBottomRightPosition($cell, $xOffset = null, $yOffset = null) + public function setBottomRightPosition($cellAddress = '', $xOffset = null, $yOffset = null) { - $this->bottomRightCellRef = $cell; + $this->bottomRightCellRef = $cellAddress; if ($xOffset !== null) { $this->setBottomRightXOffset($xOffset); } @@ -551,19 +564,22 @@ class Chart ]; } - public function setBottomRightCell($cell) + /** + * Set the Bottom Right cell for the chart. + * + * @return $this + */ + public function setBottomRightCell(string $cellAddress = '') { - $this->bottomRightCellRef = $cell; + $this->bottomRightCellRef = $cellAddress; return $this; } /** * Get the cell address where the bottom right of the chart is fixed. - * - * @return string */ - public function getBottomRightCell() + public function getBottomRightCell(): string { return $this->bottomRightCellRef; } @@ -602,6 +618,11 @@ class Chart ]; } + /** + * @param int $xOffset + * + * @return $this + */ public function setBottomRightXOffset($xOffset) { $this->bottomRightXOffset = $xOffset; @@ -609,11 +630,16 @@ class Chart return $this; } - public function getBottomRightXOffset() + public function getBottomRightXOffset(): int { return $this->bottomRightXOffset; } + /** + * @param int $yOffset + * + * @return $this + */ public function setBottomRightYOffset($yOffset) { $this->bottomRightYOffset = $yOffset; @@ -621,14 +647,14 @@ class Chart return $this; } - public function getBottomRightYOffset() + public function getBottomRightYOffset(): int { return $this->bottomRightYOffset; } public function refresh(): void { - if ($this->worksheet !== null) { + if ($this->worksheet !== null && $this->plotArea !== null) { $this->plotArea->refresh($this->worksheet); } } @@ -656,6 +682,104 @@ class Chart $renderer = new $libraryName($this); - return $renderer->render($outputDestination); + return $renderer->render($outputDestination); // @phpstan-ignore-line + } + + public function getRotX(): ?int + { + return $this->rotX; + } + + public function setRotX(?int $rotX): self + { + $this->rotX = $rotX; + + return $this; + } + + public function getRotY(): ?int + { + return $this->rotY; + } + + public function setRotY(?int $rotY): self + { + $this->rotY = $rotY; + + return $this; + } + + public function getRAngAx(): ?int + { + return $this->rAngAx; + } + + public function setRAngAx(?int $rAngAx): self + { + $this->rAngAx = $rAngAx; + + return $this; + } + + public function getPerspective(): ?int + { + return $this->perspective; + } + + public function setPerspective(?int $perspective): self + { + $this->perspective = $perspective; + + return $this; + } + + public function getOneCellAnchor(): bool + { + return $this->oneCellAnchor; + } + + public function setOneCellAnchor(bool $oneCellAnchor): self + { + $this->oneCellAnchor = $oneCellAnchor; + + return $this; + } + + public function getAutoTitleDeleted(): bool + { + return $this->autoTitleDeleted; + } + + public function setAutoTitleDeleted(bool $autoTitleDeleted): self + { + $this->autoTitleDeleted = $autoTitleDeleted; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getRoundedCorners(): bool + { + return $this->roundedCorners; + } + + public function setRoundedCorners(?bool $roundedCorners): self + { + if ($roundedCorners !== null) { + $this->roundedCorners = $roundedCorners; + } + + return $this; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php new file mode 100644 index 00000000000..87f31020092 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/ChartColor.php @@ -0,0 +1,177 @@ +setColorPropertiesArray($value); + } else { + $this->setColorProperties($value, $alpha, $type, $brightness); + } + } + + public function getValue(): string + { + return $this->value; + } + + public function setValue(string $value): self + { + $this->value = $value; + + return $this; + } + + public function getType(): string + { + return $this->type; + } + + public function setType(string $type): self + { + $this->type = $type; + + return $this; + } + + public function getAlpha(): ?int + { + return $this->alpha; + } + + public function setAlpha(?int $alpha): self + { + $this->alpha = $alpha; + + return $this; + } + + public function getBrightness(): ?int + { + return $this->brightness; + } + + public function setBrightness(?int $brightness): self + { + $this->brightness = $brightness; + + return $this; + } + + /** + * @param null|float|int|string $alpha + * @param null|float|int|string $brightness + */ + public function setColorProperties(?string $color, $alpha = null, ?string $type = null, $brightness = null): self + { + if (empty($type) && !empty($color)) { + if (substr($color, 0, 1) === '*') { + $type = 'schemeClr'; + $color = substr($color, 1); + } elseif (substr($color, 0, 1) === '/') { + $type = 'prstClr'; + $color = substr($color, 1); + } elseif (preg_match('/^[0-9A-Fa-f]{6}$/', $color) === 1) { + $type = 'srgbClr'; + } + } + if ($color !== null) { + $this->setValue("$color"); + } + if ($type !== null) { + $this->setType($type); + } + if ($alpha === null) { + $this->setAlpha(null); + } elseif (is_numeric($alpha)) { + $this->setAlpha((int) $alpha); + } + if ($brightness === null) { + $this->setBrightness(null); + } elseif (is_numeric($brightness)) { + $this->setBrightness((int) $brightness); + } + + return $this; + } + + public function setColorPropertiesArray(array $color): self + { + return $this->setColorProperties( + $color['value'] ?? '', + $color['alpha'] ?? null, + $color['type'] ?? null, + $color['brightness'] ?? null + ); + } + + public function isUsable(): bool + { + return $this->type !== '' && $this->value !== ''; + } + + /** + * Get Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getColorProperty($propertyName) + { + $retVal = null; + if ($propertyName === 'value') { + $retVal = $this->value; + } elseif ($propertyName === 'type') { + $retVal = $this->type; + } elseif ($propertyName === 'alpha') { + $retVal = $this->alpha; + } elseif ($propertyName === 'brightness') { + $retVal = $this->brightness; + } + + return $retVal; + } + + public static function alphaToXml(int $alpha): string + { + return (string) (100 - $alpha) . '000'; + } + + /** + * @param float|int|string $alpha + */ + public static function alphaFromXml($alpha): int + { + return 100 - ((int) $alpha / 1000); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php index 067d30e548f..5d33e96d093 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeries.php @@ -94,7 +94,7 @@ class DataSeries private $plotCategory = []; /** - * Smooth Line. + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. * * @var bool */ @@ -107,6 +107,13 @@ class DataSeries */ private $plotValues = []; + /** + * Plot Bubble Sizes. + * + * @var DataSeriesValues[] + */ + private $plotBubbleSizes = []; + /** * Create a new DataSeries. * @@ -127,12 +134,12 @@ class DataSeries $this->plotOrder = $plotOrder; $keys = array_keys($plotValues); $this->plotValues = $plotValues; - if ((count($plotLabel) == 0) || ($plotLabel[$keys[0]] === null)) { + if (!isset($plotLabel[$keys[0]])) { $plotLabel[$keys[0]] = new DataSeriesValues(); } $this->plotLabel = $plotLabel; - if ((count($plotCategory) == 0) || ($plotCategory[$keys[0]] === null)) { + if (!isset($plotCategory[$keys[0]])) { $plotCategory[$keys[0]] = new DataSeriesValues(); } $this->plotCategory = $plotCategory; @@ -250,8 +257,6 @@ class DataSeries $keys = array_keys($this->plotLabel); if (in_array($index, $keys)) { return $this->plotLabel[$index]; - } elseif (isset($keys[$index])) { - return $this->plotLabel[$keys[$index]]; } return false; @@ -332,13 +337,33 @@ class DataSeries $keys = array_keys($this->plotValues); if (in_array($index, $keys)) { return $this->plotValues[$index]; - } elseif (isset($keys[$index])) { - return $this->plotValues[$keys[$index]]; } return false; } + /** + * Get Plot Bubble Sizes. + * + * @return DataSeriesValues[] + */ + public function getPlotBubbleSizes(): array + { + return $this->plotBubbleSizes; + } + + /** + * Set Plot Bubble Sizes. + * + * @param DataSeriesValues[] $plotBubbleSizes + */ + public function setPlotBubbleSizes(array $plotBubbleSizes): self + { + $this->plotBubbleSizes = $plotBubbleSizes; + + return $this; + } + /** * Get Number of Plot Series. * diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeriesValues.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeriesValues.php index 88063336bf5..cd166b23b53 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeriesValues.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/DataSeriesValues.php @@ -7,12 +7,12 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class DataSeriesValues +class DataSeriesValues extends Properties { const DATASERIES_TYPE_STRING = 'String'; const DATASERIES_TYPE_NUMBER = 'Number'; - private static $dataTypeValues = [ + private const DATA_TYPE_VALUES = [ self::DATASERIES_TYPE_STRING, self::DATASERIES_TYPE_NUMBER, ]; @@ -27,7 +27,7 @@ class DataSeriesValues /** * Series Data Source. * - * @var string + * @var ?string */ private $dataSource; @@ -45,6 +45,19 @@ class DataSeriesValues */ private $pointMarker; + /** @var ChartColor */ + private $markerFillColor; + + /** @var ChartColor */ + private $markerBorderColor; + + /** + * Series Point Size. + * + * @var int + */ + private $pointSize = 3; + /** * Point Count (The number of datapoints in the dataseries). * @@ -62,16 +75,21 @@ class DataSeriesValues /** * Fill color (can be array with colors if dataseries have custom colors). * - * @var string|string[] + * @var null|ChartColor|ChartColor[] */ private $fillColor; - /** - * Line Width. - * - * @var int - */ - private $lineWidth = 12700; + /** @var bool */ + private $scatterLines = true; + + /** @var bool */ + private $bubble3D = false; + + /** @var ?Layout */ + private $labelLayout; + + /** @var TrendLine[] */ + private $trendLines = []; /** * Create a new DataSeriesValues object. @@ -82,17 +100,26 @@ class DataSeriesValues * @param int $pointCount * @param mixed $dataValues * @param null|mixed $marker - * @param null|string|string[] $fillColor + * @param null|ChartColor|ChartColor[]|string|string[] $fillColor + * @param string $pointSize */ - public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null) + public function __construct($dataType = self::DATASERIES_TYPE_NUMBER, $dataSource = null, $formatCode = null, $pointCount = 0, $dataValues = [], $marker = null, $fillColor = null, $pointSize = '3') { + parent::__construct(); + $this->markerFillColor = new ChartColor(); + $this->markerBorderColor = new ChartColor(); $this->setDataType($dataType); $this->dataSource = $dataSource; $this->formatCode = $formatCode; $this->pointCount = $pointCount; $this->dataValues = $dataValues; $this->pointMarker = $marker; - $this->fillColor = $fillColor; + if ($fillColor !== null) { + $this->setFillColor($fillColor); + } + if (is_numeric($pointSize)) { + $this->pointSize = (int) $pointSize; + } } /** @@ -119,7 +146,7 @@ class DataSeriesValues */ public function setDataType($dataType) { - if (!in_array($dataType, self::$dataTypeValues)) { + if (!in_array($dataType, self::DATA_TYPE_VALUES)) { throw new Exception('Invalid datatype for chart data series values'); } $this->dataType = $dataType; @@ -130,7 +157,7 @@ class DataSeriesValues /** * Get Series Data Source (formula). * - * @return string + * @return ?string */ public function getDataSource() { @@ -140,7 +167,7 @@ class DataSeriesValues /** * Set Series Data Source (formula). * - * @param string $dataSource + * @param ?string $dataSource * * @return $this */ @@ -175,6 +202,36 @@ class DataSeriesValues return $this; } + public function getMarkerFillColor(): ChartColor + { + return $this->markerFillColor; + } + + public function getMarkerBorderColor(): ChartColor + { + return $this->markerBorderColor; + } + + /** + * Get Point Size. + */ + public function getPointSize(): int + { + return $this->pointSize; + } + + /** + * Set Point Size. + * + * @return $this + */ + public function setPointSize(int $size = 3) + { + $this->pointSize = $size; + + return $this; + } + /** * Get Series Format Code. * @@ -209,6 +266,51 @@ class DataSeriesValues return $this->pointCount; } + /** + * Get fill color object. + * + * @return null|ChartColor|ChartColor[] + */ + public function getFillColorObject() + { + return $this->fillColor; + } + + private function stringToChartColor(string $fillString): ChartColor + { + $value = $type = ''; + if (substr($fillString, 0, 1) === '*') { + $type = 'schemeClr'; + $value = substr($fillString, 1); + } elseif (substr($fillString, 0, 1) === '/') { + $type = 'prstClr'; + $value = substr($fillString, 1); + } elseif ($fillString !== '') { + $type = 'srgbClr'; + $value = $fillString; + $this->validateColor($value); + } + + return new ChartColor($value, null, $type); + } + + private function chartColorToString(ChartColor $chartColor): string + { + $type = (string) $chartColor->getColorProperty('type'); + $value = (string) $chartColor->getColorProperty('value'); + if ($type === '' || $value === '') { + return ''; + } + if ($type === 'schemeClr') { + return "*$value"; + } + if ($type === 'prstClr') { + return "/$value"; + } + + return $value; + } + /** * Get fill color. * @@ -216,26 +318,44 @@ class DataSeriesValues */ public function getFillColor() { - return $this->fillColor; + if ($this->fillColor === null) { + return ''; + } + if (is_array($this->fillColor)) { + $array = []; + foreach ($this->fillColor as $chartColor) { + $array[] = $this->chartColorToString($chartColor); + } + + return $array; + } + + return $this->chartColorToString($this->fillColor); } /** * Set fill color for series. * - * @param string|string[] $color HEX color or array with HEX colors + * @param ChartColor|ChartColor[]|string|string[] $color HEX color or array with HEX colors * * @return DataSeriesValues */ public function setFillColor($color) { if (is_array($color)) { - foreach ($color as $colorValue) { - $this->validateColor($colorValue); + $this->fillColor = []; + foreach ($color as $fillString) { + if ($fillString instanceof ChartColor) { + $this->fillColor[] = $fillString; + } else { + $this->fillColor[] = $this->stringToChartColor($fillString); + } } + } elseif ($color instanceof ChartColor) { + $this->fillColor = $color; } else { - $this->validateColor($color); + $this->fillColor = $this->stringToChartColor($color); } - $this->fillColor = $color; return $this; } @@ -259,24 +379,23 @@ class DataSeriesValues /** * Get line width for series. * - * @return int + * @return null|float|int */ public function getLineWidth() { - return $this->lineWidth; + return $this->lineStyleProperties['width']; } /** * Set line width for the series. * - * @param int $width + * @param null|float|int $width * * @return $this */ public function setLineWidth($width) { - $minWidth = 12700; - $this->lineWidth = max($minWidth, $width); + $this->lineStyleProperties['width'] = $width; return $this; } @@ -288,7 +407,7 @@ class DataSeriesValues */ public function isMultiLevelSeries() { - if (count($this->dataValues) > 0) { + if (!empty($this->dataValues)) { return is_array(array_values($this->dataValues)[0]); } @@ -352,7 +471,7 @@ class DataSeriesValues return $this; } - public function refresh(Worksheet $worksheet, $flatten = true): void + public function refresh(Worksheet $worksheet, bool $flatten = true): void { if ($this->dataSource !== null) { $calcEngine = Calculation::getInstance($worksheet->getParent()); @@ -394,4 +513,83 @@ class DataSeriesValues $this->pointCount = count($this->dataValues); } } + + public function getScatterLines(): bool + { + return $this->scatterLines; + } + + public function setScatterLines(bool $scatterLines): self + { + $this->scatterLines = $scatterLines; + + return $this; + } + + public function getBubble3D(): bool + { + return $this->bubble3D; + } + + public function setBubble3D(bool $bubble3D): self + { + $this->bubble3D = $bubble3D; + + return $this; + } + + /** + * Smooth Line. Must be specified for both DataSeries and DataSeriesValues. + * + * @var bool + */ + private $smoothLine; + + /** + * Get Smooth Line. + * + * @return bool + */ + public function getSmoothLine() + { + return $this->smoothLine; + } + + /** + * Set Smooth Line. + * + * @param bool $smoothLine + * + * @return $this + */ + public function setSmoothLine($smoothLine) + { + $this->smoothLine = $smoothLine; + + return $this; + } + + public function getLabelLayout(): ?Layout + { + return $this->labelLayout; + } + + public function setLabelLayout(?Layout $labelLayout): self + { + $this->labelLayout = $labelLayout; + + return $this; + } + + public function setTrendLines(array $trendLines): self + { + $this->trendLines = $trendLines; + + return $this; + } + + public function getTrendLines(): array + { + return $this->trendLines; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/GridLines.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/GridLines.php index 84af3ada5a7..8b86ccbdeea 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/GridLines.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/GridLines.php @@ -10,445 +10,4 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ class GridLines extends Properties { - /** - * Properties of Class: - * Object State (State for Minor Tick Mark) @var bool - * Line Properties @var array of mixed - * Shadow Properties @var array of mixed - * Glow Properties @var array of mixed - * Soft Properties @var array of mixed. - */ - private $objectState = false; - - private $lineProperties = [ - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => null, - 'alpha' => 0, - ], - 'style' => [ - 'width' => '9525', - 'compound' => self::LINE_STYLE_COMPOUND_SIMPLE, - 'dash' => self::LINE_STYLE_DASH_SOLID, - 'cap' => self::LINE_STYLE_CAP_FLAT, - 'join' => self::LINE_STYLE_JOIN_BEVEL, - 'arrow' => [ - 'head' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_5, - ], - 'end' => [ - 'type' => self::LINE_STYLE_ARROW_TYPE_NOARROW, - 'size' => self::LINE_STYLE_ARROW_SIZE_8, - ], - ], - ], - ]; - - private $shadowProperties = [ - 'presets' => self::SHADOW_PRESETS_NOSHADOW, - 'effect' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 85, - ], - 'size' => [ - 'sx' => null, - 'sy' => null, - 'kx' => null, - ], - 'blur' => null, - 'direction' => null, - 'distance' => null, - 'algn' => null, - 'rotWithShape' => null, - ]; - - private $glowProperties = [ - 'size' => null, - 'color' => [ - 'type' => self::EXCEL_COLOR_TYPE_STANDARD, - 'value' => 'black', - 'alpha' => 40, - ], - ]; - - private $softEdges = [ - 'size' => null, - ]; - - /** - * Get Object State. - * - * @return bool - */ - public function getObjectState() - { - return $this->objectState; - } - - /** - * Change Object State to True. - * - * @return $this - */ - private function activateObject() - { - $this->objectState = true; - - return $this; - } - - /** - * Set Line Color Properties. - * - * @param string $value - * @param int $alpha - * @param string $colorType - */ - public function setLineColorProperties($value, $alpha = 0, $colorType = self::EXCEL_COLOR_TYPE_STANDARD): void - { - $this->activateObject() - ->lineProperties['color'] = $this->setColorProperties( - $value, - $alpha, - $colorType - ); - } - - /** - * Set Line Color Properties. - * - * @param float $lineWidth - * @param string $compoundType - * @param string $dashType - * @param string $capType - * @param string $joinType - * @param string $headArrowType - * @param string $headArrowSize - * @param string $endArrowType - * @param string $endArrowSize - */ - public function setLineStyleProperties($lineWidth = null, $compoundType = null, $dashType = null, $capType = null, $joinType = null, $headArrowType = null, $headArrowSize = null, $endArrowType = null, $endArrowSize = null): void - { - $this->activateObject(); - ($lineWidth !== null) - ? $this->lineProperties['style']['width'] = $this->getExcelPointsWidth((float) $lineWidth) - : null; - ($compoundType !== null) - ? $this->lineProperties['style']['compound'] = (string) $compoundType - : null; - ($dashType !== null) - ? $this->lineProperties['style']['dash'] = (string) $dashType - : null; - ($capType !== null) - ? $this->lineProperties['style']['cap'] = (string) $capType - : null; - ($joinType !== null) - ? $this->lineProperties['style']['join'] = (string) $joinType - : null; - ($headArrowType !== null) - ? $this->lineProperties['style']['arrow']['head']['type'] = (string) $headArrowType - : null; - ($headArrowSize !== null) - ? $this->lineProperties['style']['arrow']['head']['size'] = (string) $headArrowSize - : null; - ($endArrowType !== null) - ? $this->lineProperties['style']['arrow']['end']['type'] = (string) $endArrowType - : null; - ($endArrowSize !== null) - ? $this->lineProperties['style']['arrow']['end']['size'] = (string) $endArrowSize - : null; - } - - /** - * Get Line Color Property. - * - * @param string $propertyName - * - * @return string - */ - public function getLineColorProperty($propertyName) - { - return $this->lineProperties['color'][$propertyName]; - } - - /** - * Get Line Style Property. - * - * @param array|string $elements - * - * @return string - */ - public function getLineStyleProperty($elements) - { - return $this->getArrayElementsValue($this->lineProperties['style'], $elements); - } - - /** - * Set Glow Properties. - * - * @param float $size - * @param string $colorValue - * @param int $colorAlpha - * @param string $colorType - */ - public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void - { - $this - ->activateObject() - ->setGlowSize($size) - ->setGlowColor($colorValue, $colorAlpha, $colorType); - } - - /** - * Get Glow Color Property. - * - * @param string $propertyName - * - * @return string - */ - public function getGlowColor($propertyName) - { - return $this->glowProperties['color'][$propertyName]; - } - - /** - * Get Glow Size. - * - * @return string - */ - public function getGlowSize() - { - return $this->glowProperties['size']; - } - - /** - * Set Glow Size. - * - * @param float $size - * - * @return $this - */ - private function setGlowSize($size) - { - $this->glowProperties['size'] = $this->getExcelPointsWidth((float) $size); - - return $this; - } - - /** - * Set Glow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setGlowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->glowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->glowProperties['color']['alpha'] = $this->getTrueAlpha((int) $alpha); - } - if ($colorType !== null) { - $this->glowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Get Line Style Arrow Parameters. - * - * @param string $arrowSelector - * @param string $propertySelector - * - * @return string - */ - public function getLineStyleArrowParameters($arrowSelector, $propertySelector) - { - return $this->getLineStyleArrowSize($this->lineProperties['style']['arrow'][$arrowSelector]['size'], $propertySelector); - } - - /** - * Set Shadow Properties. - * - * @param int $presets - * @param string $colorValue - * @param string $colorType - * @param string $colorAlpha - * @param string $blur - * @param int $angle - * @param float $distance - */ - public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void - { - $this->activateObject() - ->setShadowPresetsProperties((int) $presets) - ->setShadowColor( - $colorValue ?? $this->shadowProperties['color']['value'], - $colorAlpha === null ? (int) $this->shadowProperties['color']['alpha'] : $this->getTrueAlpha($colorAlpha), - $colorType ?? $this->shadowProperties['color']['type'] - ) - ->setShadowBlur((float) $blur) - ->setShadowAngle($angle) - ->setShadowDistance($distance); - } - - /** - * Set Shadow Presets Properties. - * - * @param int $presets - * - * @return $this - */ - private function setShadowPresetsProperties($presets) - { - $this->shadowProperties['presets'] = $presets; - $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); - - return $this; - } - - /** - * Set Shadow Properties Values. - * - * @param mixed $reference - * - * @return $this - */ - private function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) - { - $base_reference = $reference; - foreach ($propertiesMap as $property_key => $property_val) { - if (is_array($property_val)) { - if ($reference === null) { - $reference = &$this->shadowProperties[$property_key]; - } else { - $reference = &$reference[$property_key]; - } - $this->setShadowPropertiesMapValues($property_val, $reference); - } else { - if ($base_reference === null) { - $this->shadowProperties[$property_key] = $property_val; - } else { - $reference[$property_key] = $property_val; - } - } - } - - return $this; - } - - /** - * Set Shadow Color. - * - * @param string $color - * @param int $alpha - * @param string $colorType - * - * @return $this - */ - private function setShadowColor($color, $alpha, $colorType) - { - if ($color !== null) { - $this->shadowProperties['color']['value'] = (string) $color; - } - if ($alpha !== null) { - $this->shadowProperties['color']['alpha'] = $this->getTrueAlpha((int) $alpha); - } - if ($colorType !== null) { - $this->shadowProperties['color']['type'] = (string) $colorType; - } - - return $this; - } - - /** - * Set Shadow Blur. - * - * @param float $blur - * - * @return $this - */ - private function setShadowBlur($blur) - { - if ($blur !== null) { - $this->shadowProperties['blur'] = (string) $this->getExcelPointsWidth($blur); - } - - return $this; - } - - /** - * Set Shadow Angle. - * - * @param int $angle - * - * @return $this - */ - private function setShadowAngle($angle) - { - if ($angle !== null) { - $this->shadowProperties['direction'] = (string) $this->getExcelPointsAngle($angle); - } - - return $this; - } - - /** - * Set Shadow Distance. - * - * @param float $distance - * - * @return $this - */ - private function setShadowDistance($distance) - { - if ($distance !== null) { - $this->shadowProperties['distance'] = (string) $this->getExcelPointsWidth($distance); - } - - return $this; - } - - /** - * Get Shadow Property. - * - * @param string|string[] $elements - * - * @return string - */ - public function getShadowProperty($elements) - { - return $this->getArrayElementsValue($this->shadowProperties, $elements); - } - - /** - * Set Soft Edges Size. - * - * @param float $size - */ - public function setSoftEdgesSize($size): void - { - if ($size !== null) { - $this->activateObject(); - $this->softEdges['size'] = (string) $this->getExcelPointsWidth($size); - } - } - - /** - * Get Soft Edges Size. - * - * @return string - */ - public function getSoftEdgesSize() - { - return $this->softEdges['size']; - } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php index cea96557d8f..3dabcc63fb7 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Layout.php @@ -53,11 +53,24 @@ class Layout */ private $height; + /** + * Position - t=top. + * + * @var string + */ + private $dLblPos = ''; + + /** @var string */ + private $numFmtCode = ''; + + /** @var bool */ + private $numFmtLinked = false; + /** * show legend key * Specifies that legend keys should be shown in data labels. * - * @var bool + * @var ?bool */ private $showLegendKey; @@ -65,7 +78,7 @@ class Layout * show value * Specifies that the value should be shown in a data label. * - * @var bool + * @var ?bool */ private $showVal; @@ -73,7 +86,7 @@ class Layout * show category name * Specifies that the category name should be shown in the data label. * - * @var bool + * @var ?bool */ private $showCatName; @@ -81,7 +94,7 @@ class Layout * show data series name * Specifies that the series name should be shown in the data label. * - * @var bool + * @var ?bool */ private $showSerName; @@ -89,14 +102,14 @@ class Layout * show percentage * Specifies that the percentage should be shown in the data label. * - * @var bool + * @var ?bool */ private $showPercent; /** * show bubble size. * - * @var bool + * @var ?bool */ private $showBubbleSize; @@ -104,10 +117,19 @@ class Layout * show leader lines * Specifies that leader lines should be shown for the data label. * - * @var bool + * @var ?bool */ private $showLeaderLines; + /** @var ?ChartColor */ + private $labelFillColor; + + /** @var ?ChartColor */ + private $labelBorderColor; + + /** @var ?ChartColor */ + private $labelFontColor; + /** * Create a new Layout. */ @@ -134,6 +156,37 @@ class Layout if (isset($layout['h'])) { $this->height = (float) $layout['h']; } + if (isset($layout['dLblPos'])) { + $this->dLblPos = (string) $layout['dLblPos']; + } + if (isset($layout['numFmtCode'])) { + $this->numFmtCode = (string) $layout['numFmtCode']; + } + $this->initBoolean($layout, 'showLegendKey'); + $this->initBoolean($layout, 'showVal'); + $this->initBoolean($layout, 'showCatName'); + $this->initBoolean($layout, 'showSerName'); + $this->initBoolean($layout, 'showPercent'); + $this->initBoolean($layout, 'showBubbleSize'); + $this->initBoolean($layout, 'showLeaderLines'); + $this->initBoolean($layout, 'numFmtLinked'); + $this->initColor($layout, 'labelFillColor'); + $this->initColor($layout, 'labelBorderColor'); + $this->initColor($layout, 'labelFontColor'); + } + + private function initBoolean(array $layout, string $name): void + { + if (isset($layout[$name])) { + $this->$name = (bool) $layout[$name]; + } + } + + private function initColor(array $layout, string $name): void + { + if (isset($layout[$name]) && $layout[$name] instanceof ChartColor) { + $this->$name = $layout[$name]; + } } /** @@ -304,12 +357,7 @@ class Layout return $this; } - /** - * Get show legend key. - * - * @return bool - */ - public function getShowLegendKey() + public function getShowLegendKey(): ?bool { return $this->showLegendKey; } @@ -317,24 +365,15 @@ class Layout /** * Set show legend key * Specifies that legend keys should be shown in data labels. - * - * @param bool $showLegendKey Show legend key - * - * @return $this */ - public function setShowLegendKey($showLegendKey) + public function setShowLegendKey(?bool $showLegendKey): self { $this->showLegendKey = $showLegendKey; return $this; } - /** - * Get show value. - * - * @return bool - */ - public function getShowVal() + public function getShowVal(): ?bool { return $this->showVal; } @@ -342,24 +381,15 @@ class Layout /** * Set show val * Specifies that the value should be shown in data labels. - * - * @param bool $showDataLabelValues Show val - * - * @return $this */ - public function setShowVal($showDataLabelValues) + public function setShowVal(?bool $showDataLabelValues): self { $this->showVal = $showDataLabelValues; return $this; } - /** - * Get show category name. - * - * @return bool - */ - public function getShowCatName() + public function getShowCatName(): ?bool { return $this->showCatName; } @@ -367,115 +397,147 @@ class Layout /** * Set show cat name * Specifies that the category name should be shown in data labels. - * - * @param bool $showCategoryName Show cat name - * - * @return $this */ - public function setShowCatName($showCategoryName) + public function setShowCatName(?bool $showCategoryName): self { $this->showCatName = $showCategoryName; return $this; } - /** - * Get show data series name. - * - * @return bool - */ - public function getShowSerName() + public function getShowSerName(): ?bool { return $this->showSerName; } /** - * Set show ser name + * Set show data series name. * Specifies that the series name should be shown in data labels. - * - * @param bool $showSeriesName Show series name - * - * @return $this */ - public function setShowSerName($showSeriesName) + public function setShowSerName(?bool $showSeriesName): self { $this->showSerName = $showSeriesName; return $this; } - /** - * Get show percentage. - * - * @return bool - */ - public function getShowPercent() + public function getShowPercent(): ?bool { return $this->showPercent; } /** - * Set show percentage + * Set show percentage. * Specifies that the percentage should be shown in data labels. - * - * @param bool $showPercentage Show percentage - * - * @return $this */ - public function setShowPercent($showPercentage) + public function setShowPercent(?bool $showPercentage): self { $this->showPercent = $showPercentage; return $this; } - /** - * Get show bubble size. - * - * @return bool - */ - public function getShowBubbleSize() + public function getShowBubbleSize(): ?bool { return $this->showBubbleSize; } /** - * Set show bubble size + * Set show bubble size. * Specifies that the bubble size should be shown in data labels. - * - * @param bool $showBubbleSize Show bubble size - * - * @return $this */ - public function setShowBubbleSize($showBubbleSize) + public function setShowBubbleSize(?bool $showBubbleSize): self { $this->showBubbleSize = $showBubbleSize; return $this; } - /** - * Get show leader lines. - * - * @return bool - */ - public function getShowLeaderLines() + public function getShowLeaderLines(): ?bool { return $this->showLeaderLines; } /** - * Set show leader lines + * Set show leader lines. * Specifies that leader lines should be shown in data labels. - * - * @param bool $showLeaderLines Show leader lines - * - * @return $this */ - public function setShowLeaderLines($showLeaderLines) + public function setShowLeaderLines(?bool $showLeaderLines): self { $this->showLeaderLines = $showLeaderLines; return $this; } + + public function getLabelFillColor(): ?ChartColor + { + return $this->labelFillColor; + } + + public function setLabelFillColor(?ChartColor $chartColor): self + { + $this->labelFillColor = $chartColor; + + return $this; + } + + public function getLabelBorderColor(): ?ChartColor + { + return $this->labelBorderColor; + } + + public function setLabelBorderColor(?ChartColor $chartColor): self + { + $this->labelBorderColor = $chartColor; + + return $this; + } + + public function getLabelFontColor(): ?ChartColor + { + return $this->labelFontColor; + } + + public function setLabelFontColor(?ChartColor $chartColor): self + { + $this->labelFontColor = $chartColor; + + return $this; + } + + public function getDLblPos(): string + { + return $this->dLblPos; + } + + public function setDLblPos(string $dLblPos): self + { + $this->dLblPos = $dLblPos; + + return $this; + } + + public function getNumFmtCode(): string + { + return $this->numFmtCode; + } + + public function setNumFmtCode(string $numFmtCode): self + { + $this->numFmtCode = $numFmtCode; + + return $this; + } + + public function getNumFmtLinked(): bool + { + return $this->numFmtLinked; + } + + public function setNumFmtLinked(bool $numFmtLinked): self + { + $this->numFmtLinked = $numFmtLinked; + + return $this; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php index 2f003cd87b8..fc16017c390 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Legend.php @@ -18,7 +18,7 @@ class Legend const POSITION_TOP = 't'; const POSITION_TOPRIGHT = 'tr'; - private static $positionXLref = [ + const POSITION_XLREF = [ self::XL_LEGEND_POSITION_BOTTOM => self::POSITION_BOTTOM, self::XL_LEGEND_POSITION_CORNER => self::POSITION_TOPRIGHT, self::XL_LEGEND_POSITION_CUSTOM => '??', @@ -44,7 +44,7 @@ class Legend /** * Legend Layout. * - * @var Layout + * @var ?Layout */ private $layout; @@ -80,7 +80,7 @@ class Legend */ public function setPosition($position) { - if (!in_array($position, self::$positionXLref)) { + if (!in_array($position, self::POSITION_XLREF)) { return false; } @@ -92,11 +92,11 @@ class Legend /** * Get legend position as an Excel internal numeric value. * - * @return int + * @return false|int */ public function getPositionXL() { - return array_search($this->position, self::$positionXLref); + return array_search($this->position, self::POSITION_XLREF); } /** @@ -108,11 +108,11 @@ class Legend */ public function setPositionXL($positionXL) { - if (!isset(self::$positionXLref[$positionXL])) { + if (!isset(self::POSITION_XLREF[$positionXL])) { return false; } - $this->position = self::$positionXLref[$positionXL]; + $this->position = self::POSITION_XLREF[$positionXL]; return true; } @@ -140,7 +140,7 @@ class Legend /** * Get Layout. * - * @return Layout + * @return ?Layout */ public function getLayout() { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php index ecb7b6c91e1..ccde4bb2aab 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/PlotArea.php @@ -6,10 +6,34 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class PlotArea { + /** + * No fill in plot area (show Excel gridlines through chart). + * + * @var bool + */ + private $noFill = false; + + /** + * PlotArea Gradient Stop list. + * Each entry is a 2-element array. + * First is position in %. + * Second is ChartColor. + * + * @var array[] + */ + private $gradientFillStops = []; + + /** + * PlotArea Gradient Angle. + * + * @var ?float + */ + private $gradientFillAngle; + /** * PlotArea Layout. * - * @var Layout + * @var ?Layout */ private $layout; @@ -31,12 +55,7 @@ class PlotArea $this->plotSeries = $plotSeries; } - /** - * Get Layout. - * - * @return Layout - */ - public function getLayout() + public function getLayout(): ?Layout { return $this->layout; } @@ -106,4 +125,42 @@ class PlotArea $plotSeries->refresh($worksheet); } } + + public function setNoFill(bool $noFill): self + { + $this->noFill = $noFill; + + return $this; + } + + public function getNoFill(): bool + { + return $this->noFill; + } + + public function setGradientFillProperties(array $gradientFillStops, ?float $gradientFillAngle): self + { + $this->gradientFillStops = $gradientFillStops; + $this->gradientFillAngle = $gradientFillAngle; + + return $this; + } + + /** + * Get gradientFillAngle. + */ + public function getGradientFillAngle(): ?float + { + return $this->gradientFillAngle; + } + + /** + * Get gradientFillStops. + * + * @return array + */ + public function getGradientFillStops() + { + return $this->gradientFillStops; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php index ef22fb5290d..f737ca08da3 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Properties.php @@ -10,10 +10,12 @@ namespace PhpOffice\PhpSpreadsheet\Chart; */ abstract class Properties { - const - EXCEL_COLOR_TYPE_STANDARD = 'prstClr'; - const EXCEL_COLOR_TYPE_SCHEME = 'schemeClr'; - const EXCEL_COLOR_TYPE_ARGB = 'srgbClr'; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_STANDARD = ChartColor::EXCEL_COLOR_TYPE_STANDARD; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_SCHEME = ChartColor::EXCEL_COLOR_TYPE_SCHEME; + /** @deprecated 1.24 use constant from ChartColor instead */ + const EXCEL_COLOR_TYPE_ARGB = ChartColor::EXCEL_COLOR_TYPE_ARGB; const AXIS_LABELS_LOW = 'low'; @@ -37,6 +39,7 @@ abstract class Properties const FORMAT_CODE_CURRENCY = '$#,##0.00'; const FORMAT_CODE_ACCOUNTING = '_($* #,##0.00_);_($* (#,##0.00);_($* "-"??_);_(@_)'; const FORMAT_CODE_DATE = 'm/d/yyyy'; + const FORMAT_CODE_DATE_ISO8601 = 'yyyy-mm-dd'; const FORMAT_CODE_TIME = '[$-F400]h:mm:ss AM/PM'; const FORMAT_CODE_PERCENTAGE = '0.00%'; const FORMAT_CODE_FRACTION = '# ?/?'; @@ -56,6 +59,8 @@ abstract class Properties const LINE_STYLE_COMPOUND_TRIPLE = 'tri'; const LINE_STYLE_DASH_SOLID = 'solid'; const LINE_STYLE_DASH_ROUND_DOT = 'sysDot'; + const LINE_STYLE_DASH_SQUARE_DOT = 'sysDash'; + /** @deprecated 1.24 use LINE_STYLE_DASH_SQUARE_DOT instead */ const LINE_STYLE_DASH_SQUERE_DOT = 'sysDash'; const LINE_STYPE_DASH_DASH = 'dash'; const LINE_STYLE_DASH_DASH_DOT = 'dashDot'; @@ -65,7 +70,7 @@ abstract class Properties const LINE_STYLE_CAP_SQUARE = 'sq'; const LINE_STYLE_CAP_ROUND = 'rnd'; const LINE_STYLE_CAP_FLAT = 'flat'; - const LINE_STYLE_JOIN_ROUND = 'bevel'; + const LINE_STYLE_JOIN_ROUND = 'round'; const LINE_STYLE_JOIN_MITER = 'miter'; const LINE_STYLE_JOIN_BEVEL = 'bevel'; const LINE_STYLE_ARROW_TYPE_NOARROW = null; @@ -110,249 +115,325 @@ abstract class Properties const SHADOW_PRESETS_PERSPECTIVE_LOWER_RIGHT = 22; const SHADOW_PRESETS_PERSPECTIVE_LOWER_LEFT = 23; - /** - * @param float $width - * - * @return float - */ - protected function getExcelPointsWidth($width) + const POINTS_WIDTH_MULTIPLIER = 12700; + const ANGLE_MULTIPLIER = 60000; // direction and size-kx size-ky + const PERCENTAGE_MULTIPLIER = 100000; // size sx and sy + + /** @var bool */ + protected $objectState = false; // used only for minor gridlines + + /** @var ?float */ + protected $glowSize; + + /** @var ChartColor */ + protected $glowColor; + + /** @var array */ + protected $softEdges = [ + 'size' => null, + ]; + + /** @var array */ + protected $shadowProperties = self::PRESETS_OPTIONS[0]; + + /** @var ChartColor */ + protected $shadowColor; + + public function __construct() { - return $width * 12700; + $this->lineColor = new ChartColor(); + $this->glowColor = new ChartColor(); + $this->shadowColor = new ChartColor(); + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); } /** - * @param float $angle + * Get Object State. * - * @return float + * @return bool */ - protected function getExcelPointsAngle($angle) + public function getObjectState() { - return $angle * 60000; + return $this->objectState; } - protected function getTrueAlpha($alpha) + /** + * Change Object State to True. + * + * @return $this + */ + public function activateObject() { - return (string) 100 - $alpha . '000'; + $this->objectState = true; + + return $this; } - protected function setColorProperties($color, $alpha, $colorType) + public static function pointsToXml(float $width): string + { + return (string) (int) ($width * self::POINTS_WIDTH_MULTIPLIER); + } + + public static function xmlToPoints(string $width): float + { + return ((float) $width) / self::POINTS_WIDTH_MULTIPLIER; + } + + public static function angleToXml(float $angle): string + { + return (string) (int) ($angle * self::ANGLE_MULTIPLIER); + } + + public static function xmlToAngle(string $angle): float + { + return ((float) $angle) / self::ANGLE_MULTIPLIER; + } + + public static function tenthOfPercentToXml(float $value): string + { + return (string) (int) ($value * self::PERCENTAGE_MULTIPLIER); + } + + public static function xmlToTenthOfPercent(string $value): float + { + return ((float) $value) / self::PERCENTAGE_MULTIPLIER; + } + + /** + * @param null|float|int|string $alpha + */ + protected function setColorProperties(?string $color, $alpha, ?string $colorType): array { return [ - 'type' => (string) $colorType, - 'value' => (string) $color, - 'alpha' => (string) $this->getTrueAlpha($alpha), + 'type' => $colorType, + 'value' => $color, + 'alpha' => ($alpha === null) ? null : (int) $alpha, ]; } - protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) + protected const PRESETS_OPTIONS = [ + //NONE + 0 => [ + 'presets' => self::SHADOW_PRESETS_NOSHADOW, + 'effect' => null, + //'color' => [ + // 'type' => ChartColor::EXCEL_COLOR_TYPE_STANDARD, + // 'value' => 'black', + // 'alpha' => 40, + //], + 'size' => [ + 'sx' => null, + 'sy' => null, + 'kx' => null, + 'ky' => null, + ], + 'blur' => null, + 'direction' => null, + 'distance' => null, + 'algn' => null, + 'rotWithShape' => null, + ], + //OUTER + 1 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + 'algn' => 'tl', + 'rotWithShape' => '0', + ], + 2 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + 'algn' => 't', + 'rotWithShape' => '0', + ], + 3 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + 'algn' => 'tr', + 'rotWithShape' => '0', + ], + 4 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'algn' => 'l', + 'rotWithShape' => '0', + ], + 5 => [ + 'effect' => 'outerShdw', + 'size' => [ + 'sx' => 102000 / self::PERCENTAGE_MULTIPLIER, + 'sy' => 102000 / self::PERCENTAGE_MULTIPLIER, + ], + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'algn' => 'ctr', + 'rotWithShape' => '0', + ], + 6 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 10800000 / self::ANGLE_MULTIPLIER, + 'algn' => 'r', + 'rotWithShape' => '0', + ], + 7 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 8 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 16200000 / self::ANGLE_MULTIPLIER, + 'rotWithShape' => '0', + ], + 9 => [ + 'effect' => 'outerShdw', + 'blur' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 38100 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + 'algn' => 'br', + 'rotWithShape' => '0', + ], + //INNER + 10 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + ], + 11 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + ], + 12 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + ], + 13 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + ], + 14 => [ + 'effect' => 'innerShdw', + 'blur' => 114300 / self::POINTS_WIDTH_MULTIPLIER, + ], + 15 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 10800000 / self::ANGLE_MULTIPLIER, + ], + 16 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + ], + 17 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 16200000 / self::ANGLE_MULTIPLIER, + ], + 18 => [ + 'effect' => 'innerShdw', + 'blur' => 63500 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 50800 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + ], + //perspective + 19 => [ + 'effect' => 'outerShdw', + 'blur' => 152400 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 317500 / self::POINTS_WIDTH_MULTIPLIER, + 'size' => [ + 'sx' => 90000 / self::PERCENTAGE_MULTIPLIER, + 'sy' => -19000 / self::PERCENTAGE_MULTIPLIER, + ], + 'direction' => 5400000 / self::ANGLE_MULTIPLIER, + 'rotWithShape' => '0', + ], + 20 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 18900000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => 23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => -1200000 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 21 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 13500000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => 23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => 1200000 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'br', + 'rotWithShape' => '0', + ], + 22 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 2700000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => -23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => -800400 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'bl', + 'rotWithShape' => '0', + ], + 23 => [ + 'effect' => 'outerShdw', + 'blur' => 76200 / self::POINTS_WIDTH_MULTIPLIER, + 'distance' => 12700 / self::POINTS_WIDTH_MULTIPLIER, + 'direction' => 8100000 / self::ANGLE_MULTIPLIER, + 'size' => [ + 'sy' => -23000 / self::PERCENTAGE_MULTIPLIER, + 'kx' => 800400 / self::ANGLE_MULTIPLIER, + ], + 'algn' => 'br', + 'rotWithShape' => '0', + ], + ]; + + protected function getShadowPresetsMap(int $presetsOption): array { - $sizes = [ - 1 => ['w' => 'sm', 'len' => 'sm'], - 2 => ['w' => 'sm', 'len' => 'med'], - 3 => ['w' => 'sm', 'len' => 'lg'], - 4 => ['w' => 'med', 'len' => 'sm'], - 5 => ['w' => 'med', 'len' => 'med'], - 6 => ['w' => 'med', 'len' => 'lg'], - 7 => ['w' => 'lg', 'len' => 'sm'], - 8 => ['w' => 'lg', 'len' => 'med'], - 9 => ['w' => 'lg', 'len' => 'lg'], - ]; - - return $sizes[$arraySelector][$arrayKaySelector]; - } - - protected function getShadowPresetsMap($presetsOption) - { - $presets_options = [ - //OUTER - 1 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '2700000', - 'algn' => 'tl', - 'rotWithShape' => '0', - ], - 2 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '5400000', - 'algn' => 't', - 'rotWithShape' => '0', - ], - 3 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '8100000', - 'algn' => 'tr', - 'rotWithShape' => '0', - ], - 4 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'algn' => 'l', - 'rotWithShape' => '0', - ], - 5 => [ - 'effect' => 'outerShdw', - 'size' => [ - 'sx' => '102000', - 'sy' => '102000', - ], - 'blur' => '63500', - 'distance' => '38100', - 'algn' => 'ctr', - 'rotWithShape' => '0', - ], - 6 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '10800000', - 'algn' => 'r', - 'rotWithShape' => '0', - ], - 7 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '18900000', - 'algn' => 'bl', - 'rotWithShape' => '0', - ], - 8 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '16200000', - 'rotWithShape' => '0', - ], - 9 => [ - 'effect' => 'outerShdw', - 'blur' => '50800', - 'distance' => '38100', - 'direction' => '13500000', - 'algn' => 'br', - 'rotWithShape' => '0', - ], - //INNER - 10 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '2700000', - ], - 11 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '5400000', - ], - 12 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '8100000', - ], - 13 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - ], - 14 => [ - 'effect' => 'innerShdw', - 'blur' => '114300', - ], - 15 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '10800000', - ], - 16 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '18900000', - ], - 17 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '16200000', - ], - 18 => [ - 'effect' => 'innerShdw', - 'blur' => '63500', - 'distance' => '50800', - 'direction' => '13500000', - ], - //perspective - 19 => [ - 'effect' => 'outerShdw', - 'blur' => '152400', - 'distance' => '317500', - 'size' => [ - 'sx' => '90000', - 'sy' => '-19000', - ], - 'direction' => '5400000', - 'rotWithShape' => '0', - ], - 20 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'direction' => '18900000', - 'size' => [ - 'sy' => '23000', - 'kx' => '-1200000', - ], - 'algn' => 'bl', - 'rotWithShape' => '0', - ], - 21 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'direction' => '13500000', - 'size' => [ - 'sy' => '23000', - 'kx' => '1200000', - ], - 'algn' => 'br', - 'rotWithShape' => '0', - ], - 22 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'distance' => '12700', - 'direction' => '2700000', - 'size' => [ - 'sy' => '-23000', - 'kx' => '-800400', - ], - 'algn' => 'bl', - 'rotWithShape' => '0', - ], - 23 => [ - 'effect' => 'outerShdw', - 'blur' => '76200', - 'distance' => '12700', - 'direction' => '8100000', - 'size' => [ - 'sy' => '-23000', - 'kx' => '800400', - ], - 'algn' => 'br', - 'rotWithShape' => '0', - ], - ]; - - return $presets_options[$presetsOption]; + return self::PRESETS_OPTIONS[$presetsOption] ?? self::PRESETS_OPTIONS[0]; } + /** + * Get value of array element. + * + * @param mixed $properties + * @param mixed $elements + * + * @return mixed + */ protected function getArrayElementsValue($properties, $elements) { $reference = &$properties; @@ -366,4 +447,539 @@ abstract class Properties return $reference; } + + /** + * Set Glow Properties. + * + * @param float $size + * @param ?string $colorValue + * @param ?int $colorAlpha + * @param ?string $colorType + */ + public function setGlowProperties($size, $colorValue = null, $colorAlpha = null, $colorType = null): void + { + $this + ->activateObject() + ->setGlowSize($size); + $this->glowColor->setColorPropertiesArray( + [ + 'value' => $colorValue, + 'type' => $colorType, + 'alpha' => $colorAlpha, + ] + ); + } + + /** + * Get Glow Property. + * + * @param array|string $property + * + * @return null|array|float|int|string + */ + public function getGlowProperty($property) + { + $retVal = null; + if ($property === 'size') { + $retVal = $this->glowSize; + } elseif ($property === 'color') { + $retVal = [ + 'value' => $this->glowColor->getColorProperty('value'), + 'type' => $this->glowColor->getColorProperty('type'), + 'alpha' => $this->glowColor->getColorProperty('alpha'), + ]; + } elseif (is_array($property) && count($property) >= 2 && $property[0] === 'color') { + $retVal = $this->glowColor->getColorProperty($property[1]); + } + + return $retVal; + } + + /** + * Get Glow Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getGlowColor($propertyName) + { + return $this->glowColor->getColorProperty($propertyName); + } + + public function getGlowColorObject(): ChartColor + { + return $this->glowColor; + } + + /** + * Get Glow Size. + * + * @return ?float + */ + public function getGlowSize() + { + return $this->glowSize; + } + + /** + * Set Glow Size. + * + * @param ?float $size + * + * @return $this + */ + protected function setGlowSize($size) + { + $this->glowSize = $size; + + return $this; + } + + /** + * Set Soft Edges Size. + * + * @param float $size + */ + public function setSoftEdges($size): void + { + if ($size !== null) { + $this->activateObject(); + $this->softEdges['size'] = $size; + } + } + + /** + * Get Soft Edges Size. + * + * @return string + */ + public function getSoftEdgesSize() + { + return $this->softEdges['size']; + } + + /** + * @param mixed $value + */ + public function setShadowProperty(string $propertyName, $value): self + { + $this->activateObject(); + if ($propertyName === 'color' && is_array($value)) { + $this->shadowColor->setColorPropertiesArray($value); + } else { + $this->shadowProperties[$propertyName] = $value; + } + + return $this; + } + + /** + * Set Shadow Properties. + * + * @param int $presets + * @param string $colorValue + * @param string $colorType + * @param null|float|int|string $colorAlpha + * @param null|float $blur + * @param null|int $angle + * @param null|float $distance + */ + public function setShadowProperties($presets, $colorValue = null, $colorType = null, $colorAlpha = null, $blur = null, $angle = null, $distance = null): void + { + $this->activateObject()->setShadowPresetsProperties((int) $presets); + if ($presets === 0) { + $this->shadowColor->setType(ChartColor::EXCEL_COLOR_TYPE_STANDARD); + $this->shadowColor->setValue('black'); + $this->shadowColor->setAlpha(40); + } + if ($colorValue !== null) { + $this->shadowColor->setValue($colorValue); + } + if ($colorType !== null) { + $this->shadowColor->setType($colorType); + } + if (is_numeric($colorAlpha)) { + $this->shadowColor->setAlpha((int) $colorAlpha); + } + $this + ->setShadowBlur($blur) + ->setShadowAngle($angle) + ->setShadowDistance($distance); + } + + /** + * Set Shadow Presets Properties. + * + * @param int $presets + * + * @return $this + */ + protected function setShadowPresetsProperties($presets) + { + $this->shadowProperties['presets'] = $presets; + $this->setShadowPropertiesMapValues($this->getShadowPresetsMap($presets)); + + return $this; + } + + protected const SHADOW_ARRAY_KEYS = ['size', 'color']; + + /** + * Set Shadow Properties Values. + * + * @param mixed $reference + * + * @return $this + */ + protected function setShadowPropertiesMapValues(array $propertiesMap, &$reference = null) + { + $base_reference = $reference; + foreach ($propertiesMap as $property_key => $property_val) { + if (is_array($property_val)) { + if (in_array($property_key, self::SHADOW_ARRAY_KEYS, true)) { + $reference = &$this->shadowProperties[$property_key]; + $this->setShadowPropertiesMapValues($property_val, $reference); + } + } else { + if ($base_reference === null) { + $this->shadowProperties[$property_key] = $property_val; + } else { + $reference[$property_key] = $property_val; + } + } + } + + return $this; + } + + /** + * Set Shadow Blur. + * + * @param ?float $blur + * + * @return $this + */ + protected function setShadowBlur($blur) + { + if ($blur !== null) { + $this->shadowProperties['blur'] = $blur; + } + + return $this; + } + + /** + * Set Shadow Angle. + * + * @param null|float|int|string $angle + * + * @return $this + */ + protected function setShadowAngle($angle) + { + if (is_numeric($angle)) { + $this->shadowProperties['direction'] = $angle; + } + + return $this; + } + + /** + * Set Shadow Distance. + * + * @param ?float $distance + * + * @return $this + */ + protected function setShadowDistance($distance) + { + if ($distance !== null) { + $this->shadowProperties['distance'] = $distance; + } + + return $this; + } + + public function getShadowColorObject(): ChartColor + { + return $this->shadowColor; + } + + /** + * Get Shadow Property. + * + * @param string|string[] $elements + * + * @return array|string + */ + public function getShadowProperty($elements) + { + if ($elements === 'color') { + return [ + 'value' => $this->shadowColor->getValue(), + 'type' => $this->shadowColor->getType(), + 'alpha' => $this->shadowColor->getAlpha(), + ]; + } + + return $this->getArrayElementsValue($this->shadowProperties, $elements); + } + + public function getShadowArray(): array + { + $array = $this->shadowProperties; + if ($this->getShadowColorObject()->isUsable()) { + $array['color'] = $this->getShadowProperty('color'); + } + + return $array; + } + + /** @var ChartColor */ + protected $lineColor; + + /** @var array */ + protected $lineStyleProperties = [ + 'width' => null, //'9525', + 'compound' => '', //self::LINE_STYLE_COMPOUND_SIMPLE, + 'dash' => '', //self::LINE_STYLE_DASH_SOLID, + 'cap' => '', //self::LINE_STYLE_CAP_FLAT, + 'join' => '', //self::LINE_STYLE_JOIN_BEVEL, + 'arrow' => [ + 'head' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_5, + 'w' => '', + 'len' => '', + ], + 'end' => [ + 'type' => '', //self::LINE_STYLE_ARROW_TYPE_NOARROW, + 'size' => '', //self::LINE_STYLE_ARROW_SIZE_8, + 'w' => '', + 'len' => '', + ], + ], + ]; + + public function copyLineStyles(self $otherProperties): void + { + $this->lineStyleProperties = $otherProperties->lineStyleProperties; + $this->lineColor = $otherProperties->lineColor; + $this->glowSize = $otherProperties->glowSize; + $this->glowColor = $otherProperties->glowColor; + $this->softEdges = $otherProperties->softEdges; + $this->shadowProperties = $otherProperties->shadowProperties; + } + + public function getLineColor(): ChartColor + { + return $this->lineColor; + } + + /** + * Set Line Color Properties. + * + * @param string $value + * @param ?int $alpha + * @param ?string $colorType + */ + public function setLineColorProperties($value, $alpha = null, $colorType = null): void + { + $this->activateObject(); + $this->lineColor->setColorPropertiesArray( + $this->setColorProperties( + $value, + $alpha, + $colorType + ) + ); + } + + /** + * Get Line Color Property. + * + * @param string $propertyName + * + * @return null|int|string + */ + public function getLineColorProperty($propertyName) + { + return $this->lineColor->getColorProperty($propertyName); + } + + /** + * Set Line Style Properties. + * + * @param null|float|int|string $lineWidth + * @param string $compoundType + * @param string $dashType + * @param string $capType + * @param string $joinType + * @param string $headArrowType + * @param string $headArrowSize + * @param string $endArrowType + * @param string $endArrowSize + * @param string $headArrowWidth + * @param string $headArrowLength + * @param string $endArrowWidth + * @param string $endArrowLength + */ + public function setLineStyleProperties($lineWidth = null, $compoundType = '', $dashType = '', $capType = '', $joinType = '', $headArrowType = '', $headArrowSize = '', $endArrowType = '', $endArrowSize = '', $headArrowWidth = '', $headArrowLength = '', $endArrowWidth = '', $endArrowLength = ''): void + { + $this->activateObject(); + if (is_numeric($lineWidth)) { + $this->lineStyleProperties['width'] = $lineWidth; + } + if ($compoundType !== '') { + $this->lineStyleProperties['compound'] = $compoundType; + } + if ($dashType !== '') { + $this->lineStyleProperties['dash'] = $dashType; + } + if ($capType !== '') { + $this->lineStyleProperties['cap'] = $capType; + } + if ($joinType !== '') { + $this->lineStyleProperties['join'] = $joinType; + } + if ($headArrowType !== '') { + $this->lineStyleProperties['arrow']['head']['type'] = $headArrowType; + } + if (array_key_exists($headArrowSize, self::ARROW_SIZES)) { + $this->lineStyleProperties['arrow']['head']['size'] = $headArrowSize; + $this->lineStyleProperties['arrow']['head']['w'] = self::ARROW_SIZES[$headArrowSize]['w']; + $this->lineStyleProperties['arrow']['head']['len'] = self::ARROW_SIZES[$headArrowSize]['len']; + } + if ($endArrowType !== '') { + $this->lineStyleProperties['arrow']['end']['type'] = $endArrowType; + } + if (array_key_exists($endArrowSize, self::ARROW_SIZES)) { + $this->lineStyleProperties['arrow']['end']['size'] = $endArrowSize; + $this->lineStyleProperties['arrow']['end']['w'] = self::ARROW_SIZES[$endArrowSize]['w']; + $this->lineStyleProperties['arrow']['end']['len'] = self::ARROW_SIZES[$endArrowSize]['len']; + } + if ($headArrowWidth !== '') { + $this->lineStyleProperties['arrow']['head']['w'] = $headArrowWidth; + } + if ($headArrowLength !== '') { + $this->lineStyleProperties['arrow']['head']['len'] = $headArrowLength; + } + if ($endArrowWidth !== '') { + $this->lineStyleProperties['arrow']['end']['w'] = $endArrowWidth; + } + if ($endArrowLength !== '') { + $this->lineStyleProperties['arrow']['end']['len'] = $endArrowLength; + } + } + + public function getLineStyleArray(): array + { + return $this->lineStyleProperties; + } + + public function setLineStyleArray(array $lineStyleProperties = []): self + { + $this->activateObject(); + $this->lineStyleProperties['width'] = $lineStyleProperties['width'] ?? null; + $this->lineStyleProperties['compound'] = $lineStyleProperties['compound'] ?? ''; + $this->lineStyleProperties['dash'] = $lineStyleProperties['dash'] ?? ''; + $this->lineStyleProperties['cap'] = $lineStyleProperties['cap'] ?? ''; + $this->lineStyleProperties['join'] = $lineStyleProperties['join'] ?? ''; + $this->lineStyleProperties['arrow']['head']['type'] = $lineStyleProperties['arrow']['head']['type'] ?? ''; + $this->lineStyleProperties['arrow']['head']['size'] = $lineStyleProperties['arrow']['head']['size'] ?? ''; + $this->lineStyleProperties['arrow']['head']['w'] = $lineStyleProperties['arrow']['head']['w'] ?? ''; + $this->lineStyleProperties['arrow']['head']['len'] = $lineStyleProperties['arrow']['head']['len'] ?? ''; + $this->lineStyleProperties['arrow']['end']['type'] = $lineStyleProperties['arrow']['end']['type'] ?? ''; + $this->lineStyleProperties['arrow']['end']['size'] = $lineStyleProperties['arrow']['end']['size'] ?? ''; + $this->lineStyleProperties['arrow']['end']['w'] = $lineStyleProperties['arrow']['end']['w'] ?? ''; + $this->lineStyleProperties['arrow']['end']['len'] = $lineStyleProperties['arrow']['end']['len'] ?? ''; + + return $this; + } + + /** + * @param mixed $value + */ + public function setLineStyleProperty(string $propertyName, $value): self + { + $this->activateObject(); + $this->lineStyleProperties[$propertyName] = $value; + + return $this; + } + + /** + * Get Line Style Property. + * + * @param array|string $elements + * + * @return string + */ + public function getLineStyleProperty($elements) + { + return $this->getArrayElementsValue($this->lineStyleProperties, $elements); + } + + protected const ARROW_SIZES = [ + 1 => ['w' => 'sm', 'len' => 'sm'], + 2 => ['w' => 'sm', 'len' => 'med'], + 3 => ['w' => 'sm', 'len' => 'lg'], + 4 => ['w' => 'med', 'len' => 'sm'], + 5 => ['w' => 'med', 'len' => 'med'], + 6 => ['w' => 'med', 'len' => 'lg'], + 7 => ['w' => 'lg', 'len' => 'sm'], + 8 => ['w' => 'lg', 'len' => 'med'], + 9 => ['w' => 'lg', 'len' => 'lg'], + ]; + + /** + * Get Line Style Arrow Size. + * + * @param int $arraySelector + * @param string $arrayKaySelector + * + * @return string + */ + protected function getLineStyleArrowSize($arraySelector, $arrayKaySelector) + { + return self::ARROW_SIZES[$arraySelector][$arrayKaySelector] ?? ''; + } + + /** + * Get Line Style Arrow Parameters. + * + * @param string $arrowSelector + * @param string $propertySelector + * + * @return string + */ + public function getLineStyleArrowParameters($arrowSelector, $propertySelector) + { + return $this->getLineStyleArrowSize($this->lineStyleProperties['arrow'][$arrowSelector]['size'], $propertySelector); + } + + /** + * Get Line Style Arrow Width. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowWidth($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'w']); + } + + /** + * Get Line Style Arrow Excel Length. + * + * @param string $arrow + * + * @return string + */ + public function getLineStyleArrowLength($arrow) + { + return $this->getLineStyleProperty(['arrow', $arrow, 'len']); + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php index 0ab70870a73..0b0164b4ecf 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php @@ -2,60 +2,20 @@ namespace PhpOffice\PhpSpreadsheet\Chart\Renderer; -use AccBarPlot; -use AccLinePlot; -use BarPlot; -use ContourPlot; -use Graph; -use GroupBarPlot; -use LinePlot; -use PhpOffice\PhpSpreadsheet\Chart\Chart; -use PhpOffice\PhpSpreadsheet\Style\NumberFormat; -use PieGraph; -use PiePlot; -use PiePlot3D; -use PiePlotC; -use RadarGraph; -use RadarPlot; -use ScatterPlot; -use Spline; -use StockPlot; - -class JpGraph implements IRenderer +/** + * Jpgraph is not oficially maintained in Composer, so the version there + * could be out of date. For that reason, all unit test requiring Jpgraph + * are skipped. So, do not measure code coverage for this class till that + * is fixed. + * + * This implementation uses abandoned package + * https://packagist.org/packages/jpgraph/jpgraph + * + * @codeCoverageIgnore + */ +class JpGraph extends JpGraphRendererBase { - private static $width = 640; - - private static $height = 480; - - private static $colourSet = [ - 'mediumpurple1', 'palegreen3', 'gold1', 'cadetblue1', - 'darkmagenta', 'coral', 'dodgerblue3', 'eggplant', - 'mediumblue', 'magenta', 'sandybrown', 'cyan', - 'firebrick1', 'forestgreen', 'deeppink4', 'darkolivegreen', - 'goldenrod2', - ]; - - private static $markSet; - - private $chart; - - private $graph; - - private static $plotColour = 0; - - private static $plotMark = 0; - - /** - * Create a new jpgraph. - */ - public function __construct(Chart $chart) - { - self::init(); - $this->graph = null; - $this->chart = $chart; - } - - private static function init(): void + protected static function init(): void { static $loaded = false; if ($loaded) { @@ -73,802 +33,6 @@ class JpGraph implements IRenderer \JpGraph\JpGraph::module('scatter'); \JpGraph\JpGraph::module('stock'); - self::$markSet = [ - 'diamond' => MARK_DIAMOND, - 'square' => MARK_SQUARE, - 'triangle' => MARK_UTRIANGLE, - 'x' => MARK_X, - 'star' => MARK_STAR, - 'dot' => MARK_FILLEDCIRCLE, - 'dash' => MARK_DTRIANGLE, - 'circle' => MARK_CIRCLE, - 'plus' => MARK_CROSS, - ]; - $loaded = true; } - - private function formatPointMarker($seriesPlot, $markerID) - { - $plotMarkKeys = array_keys(self::$markSet); - if ($markerID === null) { - // Use default plot marker (next marker in the series) - self::$plotMark %= count(self::$markSet); - $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); - } elseif ($markerID !== 'none') { - // Use specified plot marker (if it exists) - if (isset(self::$markSet[$markerID])) { - $seriesPlot->mark->SetType(self::$markSet[$markerID]); - } else { - // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) - self::$plotMark %= count(self::$markSet); - $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); - } - } else { - // Hide plot marker - $seriesPlot->mark->Hide(); - } - $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); - $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - - return $seriesPlot; - } - - private function formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation = '') - { - $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode(); - if ($datasetLabelFormatCode !== null) { - // Retrieve any label formatting code - $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); - } - - $testCurrentIndex = 0; - foreach ($datasetLabels as $i => $datasetLabel) { - if (is_array($datasetLabel)) { - if ($rotation == 'bar') { - $datasetLabels[$i] = implode(' ', $datasetLabel); - } else { - $datasetLabel = array_reverse($datasetLabel); - $datasetLabels[$i] = implode("\n", $datasetLabel); - } - } else { - // Format labels according to any formatting code - if ($datasetLabelFormatCode !== null) { - $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); - } - } - ++$testCurrentIndex; - } - - return $datasetLabels; - } - - private function percentageSumCalculation($groupID, $seriesCount) - { - $sumValues = []; - // Adjust our values to a percentage value across all series in the group - for ($i = 0; $i < $seriesCount; ++$i) { - if ($i == 0) { - $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - } else { - $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - foreach ($nextValues as $k => $value) { - if (isset($sumValues[$k])) { - $sumValues[$k] += $value; - } else { - $sumValues[$k] = $value; - } - } - } - } - - return $sumValues; - } - - private function percentageAdjustValues($dataValues, $sumValues) - { - foreach ($dataValues as $k => $dataValue) { - $dataValues[$k] = $dataValue / $sumValues[$k] * 100; - } - - return $dataValues; - } - - private function getCaption($captionElement) - { - // Read any caption - $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; - // Test if we have a title caption to display - if ($caption !== null) { - // If we do, it could be a plain string or an array - if (is_array($caption)) { - // Implode an array to a plain string - $caption = implode('', $caption); - } - } - - return $caption; - } - - private function renderTitle(): void - { - $title = $this->getCaption($this->chart->getTitle()); - if ($title !== null) { - $this->graph->title->Set($title); - } - } - - private function renderLegend(): void - { - $legend = $this->chart->getLegend(); - if ($legend !== null) { - $legendPosition = $legend->getPosition(); - switch ($legendPosition) { - case 'r': - $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right - $this->graph->legend->SetColumns(1); - - break; - case 'l': - $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left - $this->graph->legend->SetColumns(1); - - break; - case 't': - $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top - - break; - case 'b': - $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom - - break; - default: - $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right - $this->graph->legend->SetColumns(1); - - break; - } - } else { - $this->graph->legend->Hide(); - } - } - - private function renderCartesianPlotArea($type = 'textlin'): void - { - $this->graph = new Graph(self::$width, self::$height); - $this->graph->SetScale($type); - - $this->renderTitle(); - - // Rotate for bar rather than column chart - $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); - $reverse = $rotation == 'bar'; - - $xAxisLabel = $this->chart->getXAxisLabel(); - if ($xAxisLabel !== null) { - $title = $this->getCaption($xAxisLabel); - if ($title !== null) { - $this->graph->xaxis->SetTitle($title, 'center'); - $this->graph->xaxis->title->SetMargin(35); - if ($reverse) { - $this->graph->xaxis->title->SetAngle(90); - $this->graph->xaxis->title->SetMargin(90); - } - } - } - - $yAxisLabel = $this->chart->getYAxisLabel(); - if ($yAxisLabel !== null) { - $title = $this->getCaption($yAxisLabel); - if ($title !== null) { - $this->graph->yaxis->SetTitle($title, 'center'); - if ($reverse) { - $this->graph->yaxis->title->SetAngle(0); - $this->graph->yaxis->title->SetMargin(-55); - } - } - } - } - - private function renderPiePlotArea(): void - { - $this->graph = new PieGraph(self::$width, self::$height); - - $this->renderTitle(); - } - - private function renderRadarPlotArea(): void - { - $this->graph = new RadarGraph(self::$width, self::$height); - $this->graph->SetScale('lin'); - - $this->renderTitle(); - } - - private function renderPlotLine($groupID, $filled = false, $combination = false, $dimensions = '2d'): void - { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - if ($grouping == 'percentStacked') { - $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); - } else { - $sumValues = []; - } - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - - if ($grouping == 'percentStacked') { - $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); - } - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - $seriesPlot = new LinePlot($dataValues); - if ($combination) { - $seriesPlot->SetBarCenter(); - } - - if ($filled) { - $seriesPlot->SetFilled(true); - $seriesPlot->SetColor('black'); - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); - } else { - // Set the appropriate plot marker - $this->formatPointMarker($seriesPlot, $marker); - } - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetLegend($dataLabel); - - $seriesPlots[] = $seriesPlot; - } - - if ($grouping == 'standard') { - $groupPlot = $seriesPlots; - } else { - $groupPlot = new AccLinePlot($seriesPlots); - } - $this->graph->Add($groupPlot); - } - - private function renderPlotBar($groupID, $dimensions = '2d'): void - { - $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); - // Rotate for bar rather than column chart - if (($groupID == 0) && ($rotation == 'bar')) { - $this->graph->Set90AndMargin(); - } - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount, $rotation); - // Rotate for bar rather than column chart - if ($rotation == 'bar') { - $datasetLabels = array_reverse($datasetLabels); - $this->graph->yaxis->SetPos('max'); - $this->graph->yaxis->SetLabelAlign('center', 'top'); - $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); - } - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - if ($grouping == 'percentStacked') { - $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); - } else { - $sumValues = []; - } - - // Loop through each data series in turn - for ($j = 0; $j < $seriesCount; ++$j) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); - if ($grouping == 'percentStacked') { - $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); - } - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - // Reverse the $dataValues order for bar rather than column chart - if ($rotation == 'bar') { - $dataValues = array_reverse($dataValues); - } - $seriesPlot = new BarPlot($dataValues); - $seriesPlot->SetColor('black'); - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); - if ($dimensions == '3d') { - $seriesPlot->SetShadow(); - } - if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) { - $dataLabel = ''; - } else { - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue(); - } - $seriesPlot->SetLegend($dataLabel); - - $seriesPlots[] = $seriesPlot; - } - // Reverse the plot order for bar rather than column chart - if (($rotation == 'bar') && ($grouping != 'percentStacked')) { - $seriesPlots = array_reverse($seriesPlots); - } - - if ($grouping == 'clustered') { - $groupPlot = new GroupBarPlot($seriesPlots); - } elseif ($grouping == 'standard') { - $groupPlot = new GroupBarPlot($seriesPlots); - } else { - $groupPlot = new AccBarPlot($seriesPlots); - if ($dimensions == '3d') { - $groupPlot->SetShadow(); - } - } - - $this->graph->Add($groupPlot); - } - - private function renderPlotScatter($groupID, $bubble): void - { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - - foreach ($dataValuesY as $k => $dataValueY) { - $dataValuesY[$k] = $k; - } - - $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); - if ($scatterStyle == 'lineMarker') { - $seriesPlot->SetLinkPoints(); - $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); - } elseif ($scatterStyle == 'smoothMarker') { - $spline = new Spline($dataValuesY, $dataValuesX); - [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); - $lplot = new LinePlot($splineDataX, $splineDataY); - $lplot->SetColor(self::$colourSet[self::$plotColour]); - - $this->graph->Add($lplot); - } - - if ($bubble) { - $this->formatPointMarker($seriesPlot, 'dot'); - $seriesPlot->mark->SetColor('black'); - $seriesPlot->mark->SetSize($bubbleSize); - } else { - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - $this->formatPointMarker($seriesPlot, $marker); - } - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetLegend($dataLabel); - - $this->graph->Add($seriesPlot); - } - } - - private function renderPlotRadar($groupID): void - { - $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); - - $dataValues = []; - foreach ($dataValuesY as $k => $dataValueY) { - $dataValues[$k] = implode(' ', array_reverse($dataValueY)); - } - $tmp = array_shift($dataValues); - $dataValues[] = $tmp; - $tmp = array_shift($dataValuesX); - $dataValuesX[] = $tmp; - - $this->graph->SetTitles(array_reverse($dataValues)); - - $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); - - $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - if ($radarStyle == 'filled') { - $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); - } - $this->formatPointMarker($seriesPlot, $marker); - $seriesPlot->SetLegend($dataLabel); - - $this->graph->Add($seriesPlot); - } - } - - private function renderPlotContour($groupID): void - { - $contourStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - - $dataValues = []; - // Loop through each data series in turn - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); - - $dataValues[$i] = $dataValuesX; - } - $seriesPlot = new ContourPlot($dataValues); - - $this->graph->Add($seriesPlot); - } - - private function renderPlotStock($groupID): void - { - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); - - $dataValues = []; - // Loop through each data series in turn and build the plot arrays - foreach ($plotOrder as $i => $v) { - $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); - foreach ($dataValuesX as $j => $dataValueX) { - $dataValues[$plotOrder[$i]][$j] = $dataValueX; - } - } - if (empty($dataValues)) { - return; - } - - $dataValuesPlot = []; - // Flatten the plot arrays to a single dimensional array to work with jpgraph - $jMax = count($dataValues[0]); - for ($j = 0; $j < $jMax; ++$j) { - for ($i = 0; $i < $seriesCount; ++$i) { - $dataValuesPlot[] = $dataValues[$i][$j]; - } - } - - // Set the x-axis labels - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - $this->graph->xaxis->SetTickLabels($datasetLabels); - } - - $seriesPlot = new StockPlot($dataValuesPlot); - $seriesPlot->SetWidth(20); - - $this->graph->Add($seriesPlot); - } - - private function renderAreaChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, true, false, $dimensions); - } - } - - private function renderLineChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotLine($i, false, false, $dimensions); - } - } - - private function renderBarChart($groupCount, $dimensions = '2d'): void - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotBar($i, $dimensions); - } - } - - private function renderScatterChart($groupCount): void - { - $this->renderCartesianPlotArea('linlin'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotScatter($i, false); - } - } - - private function renderBubbleChart($groupCount): void - { - $this->renderCartesianPlotArea('linlin'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotScatter($i, true); - } - } - - private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void - { - $this->renderPiePlotArea(); - - $iLimit = ($multiplePlots) ? $groupCount : 1; - for ($groupID = 0; $groupID < $iLimit; ++$groupID) { - $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); - $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); - $datasetLabels = []; - if ($groupID == 0) { - $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); - if ($labelCount > 0) { - $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); - $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $labelCount); - } - } - - $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); - $seriesPlots = []; - // For pie charts, we only display the first series: doughnut charts generally display all series - $jLimit = ($multiplePlots) ? $seriesCount : 1; - // Loop through each data series in turn - for ($j = 0; $j < $jLimit; ++$j) { - $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); - - // Fill in any missing values in the $dataValues array - $testCurrentIndex = 0; - foreach ($dataValues as $k => $dataValue) { - while ($k != $testCurrentIndex) { - $dataValues[$testCurrentIndex] = null; - ++$testCurrentIndex; - } - ++$testCurrentIndex; - } - - if ($dimensions == '3d') { - $seriesPlot = new PiePlot3D($dataValues); - } else { - if ($doughnut) { - $seriesPlot = new PiePlotC($dataValues); - } else { - $seriesPlot = new PiePlot($dataValues); - } - } - - if ($multiplePlots) { - $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); - } - - if ($doughnut) { - $seriesPlot->SetMidColor('white'); - } - - $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); - if (count($datasetLabels) > 0) { - $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); - } - if ($dimensions != '3d') { - $seriesPlot->SetGuideLines(false); - } - if ($j == 0) { - if ($exploded) { - $seriesPlot->ExplodeAll(); - } - $seriesPlot->SetLegends($datasetLabels); - } - - $this->graph->Add($seriesPlot); - } - } - } - - private function renderRadarChart($groupCount): void - { - $this->renderRadarPlotArea(); - - for ($groupID = 0; $groupID < $groupCount; ++$groupID) { - $this->renderPlotRadar($groupID); - } - } - - private function renderStockChart($groupCount): void - { - $this->renderCartesianPlotArea('intint'); - - for ($groupID = 0; $groupID < $groupCount; ++$groupID) { - $this->renderPlotStock($groupID); - } - } - - private function renderContourChart($groupCount, $dimensions): void - { - $this->renderCartesianPlotArea('intint'); - - for ($i = 0; $i < $groupCount; ++$i) { - $this->renderPlotContour($i); - } - } - - private function renderCombinationChart($groupCount, $dimensions, $outputDestination) - { - $this->renderCartesianPlotArea(); - - for ($i = 0; $i < $groupCount; ++$i) { - $dimensions = null; - $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); - switch ($chartType) { - case 'area3DChart': - $dimensions = '3d'; - // no break - case 'areaChart': - $this->renderPlotLine($i, true, true, $dimensions); - - break; - case 'bar3DChart': - $dimensions = '3d'; - // no break - case 'barChart': - $this->renderPlotBar($i, $dimensions); - - break; - case 'line3DChart': - $dimensions = '3d'; - // no break - case 'lineChart': - $this->renderPlotLine($i, false, true, $dimensions); - - break; - case 'scatterChart': - $this->renderPlotScatter($i, false); - - break; - case 'bubbleChart': - $this->renderPlotScatter($i, true); - - break; - default: - $this->graph = null; - - return false; - } - } - - $this->renderLegend(); - - $this->graph->Stroke($outputDestination); - - return true; - } - - public function render($outputDestination) - { - self::$plotColour = 0; - - $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); - - $dimensions = null; - if ($groupCount == 1) { - $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); - } else { - $chartTypes = []; - for ($i = 0; $i < $groupCount; ++$i) { - $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); - } - $chartTypes = array_unique($chartTypes); - if (count($chartTypes) == 1) { - $chartType = array_pop($chartTypes); - } elseif (count($chartTypes) == 0) { - echo 'Chart is not yet implemented
'; - - return false; - } else { - return $this->renderCombinationChart($groupCount, $dimensions, $outputDestination); - } - } - - switch ($chartType) { - case 'area3DChart': - $dimensions = '3d'; - // no break - case 'areaChart': - $this->renderAreaChart($groupCount, $dimensions); - - break; - case 'bar3DChart': - $dimensions = '3d'; - // no break - case 'barChart': - $this->renderBarChart($groupCount, $dimensions); - - break; - case 'line3DChart': - $dimensions = '3d'; - // no break - case 'lineChart': - $this->renderLineChart($groupCount, $dimensions); - - break; - case 'pie3DChart': - $dimensions = '3d'; - // no break - case 'pieChart': - $this->renderPieChart($groupCount, $dimensions, false, false); - - break; - case 'doughnut3DChart': - $dimensions = '3d'; - // no break - case 'doughnutChart': - $this->renderPieChart($groupCount, $dimensions, true, true); - - break; - case 'scatterChart': - $this->renderScatterChart($groupCount); - - break; - case 'bubbleChart': - $this->renderBubbleChart($groupCount); - - break; - case 'radarChart': - $this->renderRadarChart($groupCount); - - break; - case 'surface3DChart': - $dimensions = '3d'; - // no break - case 'surfaceChart': - $this->renderContourChart($groupCount, $dimensions); - - break; - case 'stockChart': - $this->renderStockChart($groupCount); - - break; - default: - echo $chartType . ' is not yet implemented
'; - - return false; - } - $this->renderLegend(); - - $this->graph->Stroke($outputDestination); - - return true; - } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php new file mode 100644 index 00000000000..cb9b544b816 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/JpGraphRendererBase.php @@ -0,0 +1,852 @@ +graph = null; + $this->chart = $chart; + + self::$markSet = [ + 'diamond' => MARK_DIAMOND, + 'square' => MARK_SQUARE, + 'triangle' => MARK_UTRIANGLE, + 'x' => MARK_X, + 'star' => MARK_STAR, + 'dot' => MARK_FILLEDCIRCLE, + 'dash' => MARK_DTRIANGLE, + 'circle' => MARK_CIRCLE, + 'plus' => MARK_CROSS, + ]; + } + + /** + * This method should be overriden in descendants to do real JpGraph library initialization. + */ + abstract protected static function init(): void; + + private function formatPointMarker($seriesPlot, $markerID) + { + $plotMarkKeys = array_keys(self::$markSet); + if ($markerID === null) { + // Use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } elseif ($markerID !== 'none') { + // Use specified plot marker (if it exists) + if (isset(self::$markSet[$markerID])) { + $seriesPlot->mark->SetType(self::$markSet[$markerID]); + } else { + // If the specified plot marker doesn't exist, use default plot marker (next marker in the series) + self::$plotMark %= count(self::$markSet); + $seriesPlot->mark->SetType(self::$markSet[$plotMarkKeys[self::$plotMark++]]); + } + } else { + // Hide plot marker + $seriesPlot->mark->Hide(); + } + $seriesPlot->mark->SetColor(self::$colourSet[self::$plotColour]); + $seriesPlot->mark->SetFillColor(self::$colourSet[self::$plotColour]); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + + return $seriesPlot; + } + + private function formatDataSetLabels($groupID, $datasetLabels, $rotation = '') + { + $datasetLabelFormatCode = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getFormatCode() ?? ''; + // Retrieve any label formatting code + $datasetLabelFormatCode = stripslashes($datasetLabelFormatCode); + + $testCurrentIndex = 0; + foreach ($datasetLabels as $i => $datasetLabel) { + if (is_array($datasetLabel)) { + if ($rotation == 'bar') { + $datasetLabels[$i] = implode(' ', $datasetLabel); + } else { + $datasetLabel = array_reverse($datasetLabel); + $datasetLabels[$i] = implode("\n", $datasetLabel); + } + } else { + // Format labels according to any formatting code + if ($datasetLabelFormatCode !== null) { + $datasetLabels[$i] = NumberFormat::toFormattedString($datasetLabel, $datasetLabelFormatCode); + } + } + ++$testCurrentIndex; + } + + return $datasetLabels; + } + + private function percentageSumCalculation($groupID, $seriesCount) + { + $sumValues = []; + // Adjust our values to a percentage value across all series in the group + for ($i = 0; $i < $seriesCount; ++$i) { + if ($i == 0) { + $sumValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + } else { + $nextValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + foreach ($nextValues as $k => $value) { + if (isset($sumValues[$k])) { + $sumValues[$k] += $value; + } else { + $sumValues[$k] = $value; + } + } + } + } + + return $sumValues; + } + + private function percentageAdjustValues($dataValues, $sumValues) + { + foreach ($dataValues as $k => $dataValue) { + $dataValues[$k] = $dataValue / $sumValues[$k] * 100; + } + + return $dataValues; + } + + private function getCaption($captionElement) + { + // Read any caption + $caption = ($captionElement !== null) ? $captionElement->getCaption() : null; + // Test if we have a title caption to display + if ($caption !== null) { + // If we do, it could be a plain string or an array + if (is_array($caption)) { + // Implode an array to a plain string + $caption = implode('', $caption); + } + } + + return $caption; + } + + private function renderTitle(): void + { + $title = $this->getCaption($this->chart->getTitle()); + if ($title !== null) { + $this->graph->title->Set($title); + } + } + + private function renderLegend(): void + { + $legend = $this->chart->getLegend(); + if ($legend !== null) { + $legendPosition = $legend->getPosition(); + switch ($legendPosition) { + case 'r': + $this->graph->legend->SetPos(0.01, 0.5, 'right', 'center'); // right + $this->graph->legend->SetColumns(1); + + break; + case 'l': + $this->graph->legend->SetPos(0.01, 0.5, 'left', 'center'); // left + $this->graph->legend->SetColumns(1); + + break; + case 't': + $this->graph->legend->SetPos(0.5, 0.01, 'center', 'top'); // top + + break; + case 'b': + $this->graph->legend->SetPos(0.5, 0.99, 'center', 'bottom'); // bottom + + break; + default: + $this->graph->legend->SetPos(0.01, 0.01, 'right', 'top'); // top-right + $this->graph->legend->SetColumns(1); + + break; + } + } else { + $this->graph->legend->Hide(); + } + } + + private function renderCartesianPlotArea($type = 'textlin'): void + { + $this->graph = new Graph(self::$width, self::$height); + $this->graph->SetScale($type); + + $this->renderTitle(); + + // Rotate for bar rather than column chart + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotDirection(); + $reverse = $rotation == 'bar'; + + $xAxisLabel = $this->chart->getXAxisLabel(); + if ($xAxisLabel !== null) { + $title = $this->getCaption($xAxisLabel); + if ($title !== null) { + $this->graph->xaxis->SetTitle($title, 'center'); + $this->graph->xaxis->title->SetMargin(35); + if ($reverse) { + $this->graph->xaxis->title->SetAngle(90); + $this->graph->xaxis->title->SetMargin(90); + } + } + } + + $yAxisLabel = $this->chart->getYAxisLabel(); + if ($yAxisLabel !== null) { + $title = $this->getCaption($yAxisLabel); + if ($title !== null) { + $this->graph->yaxis->SetTitle($title, 'center'); + if ($reverse) { + $this->graph->yaxis->title->SetAngle(0); + $this->graph->yaxis->title->SetMargin(-55); + } + } + } + } + + private function renderPiePlotArea(): void + { + $this->graph = new PieGraph(self::$width, self::$height); + + $this->renderTitle(); + } + + private function renderRadarPlotArea(): void + { + $this->graph = new RadarGraph(self::$width, self::$height); + $this->graph->SetScale('lin'); + + $this->renderTitle(); + } + + private function renderPlotLine($groupID, $filled = false, $combination = false): void + { + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$i]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointMarker(); + + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + $seriesPlot = new LinePlot($dataValues); + if ($combination) { + $seriesPlot->SetBarCenter(); + } + + if ($filled) { + $seriesPlot->SetFilled(true); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + } else { + // Set the appropriate plot marker + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($index)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + + if ($grouping == 'standard') { + $groupPlot = $seriesPlots; + } else { + $groupPlot = new AccLinePlot($seriesPlots); + } + $this->graph->Add($groupPlot); + } + + private function renderPlotBar($groupID, $dimensions = '2d'): void + { + $rotation = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotDirection(); + // Rotate for bar rather than column chart + if (($groupID == 0) && ($rotation == 'bar')) { + $this->graph->Set90AndMargin(); + } + $grouping = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotGrouping(); + + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[0]; + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels, $rotation); + // Rotate for bar rather than column chart + if ($rotation == 'bar') { + $datasetLabels = array_reverse($datasetLabels); + $this->graph->yaxis->SetPos('max'); + $this->graph->yaxis->SetLabelAlign('center', 'top'); + $this->graph->yaxis->SetLabelSide(SIDE_RIGHT); + } + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $seriesPlots = []; + if ($grouping == 'percentStacked') { + $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; + } + + // Loop through each data series in turn + for ($j = 0; $j < $seriesCount; ++$j) { + $index = array_keys($this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder())[$j]; + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($index)->getDataValues(); + if ($grouping == 'percentStacked') { + $dataValues = $this->percentageAdjustValues($dataValues, $sumValues); + } + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + // Reverse the $dataValues order for bar rather than column chart + if ($rotation == 'bar') { + $dataValues = array_reverse($dataValues); + } + $seriesPlot = new BarPlot($dataValues); + $seriesPlot->SetColor('black'); + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour++]); + if ($dimensions == '3d') { + $seriesPlot->SetShadow(); + } + if (!$this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)) { + $dataLabel = ''; + } else { + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($j)->getDataValue(); + } + $seriesPlot->SetLegend($dataLabel); + + $seriesPlots[] = $seriesPlot; + } + // Reverse the plot order for bar rather than column chart + if (($rotation == 'bar') && ($grouping != 'percentStacked')) { + $seriesPlots = array_reverse($seriesPlots); + } + + if ($grouping == 'clustered') { + $groupPlot = new GroupBarPlot($seriesPlots); + } elseif ($grouping == 'standard') { + $groupPlot = new GroupBarPlot($seriesPlots); + } else { + $groupPlot = new AccBarPlot($seriesPlots); + if ($dimensions == '3d') { + $groupPlot->SetShadow(); + } + } + + $this->graph->Add($groupPlot); + } + + private function renderPlotScatter($groupID, $bubble): void + { + $scatterStyle = $bubbleSize = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + foreach ($dataValuesY as $k => $dataValueY) { + $dataValuesY[$k] = $k; + } + + $seriesPlot = new ScatterPlot($dataValuesX, $dataValuesY); + if ($scatterStyle == 'lineMarker') { + $seriesPlot->SetLinkPoints(); + $seriesPlot->link->SetColor(self::$colourSet[self::$plotColour]); + } elseif ($scatterStyle == 'smoothMarker') { + $spline = new Spline($dataValuesY, $dataValuesX); + [$splineDataY, $splineDataX] = $spline->Get(count($dataValuesX) * self::$width / 20); + $lplot = new LinePlot($splineDataX, $splineDataY); + $lplot->SetColor(self::$colourSet[self::$plotColour]); + + $this->graph->Add($lplot); + } + + if ($bubble) { + $this->formatPointMarker($seriesPlot, 'dot'); + $seriesPlot->mark->SetColor('black'); + $seriesPlot->mark->SetSize($bubbleSize); + } else { + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + $this->formatPointMarker($seriesPlot, $marker); + } + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotRadar($groupID): void + { + $radarStyle = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesY = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex($i)->getDataValues(); + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + $marker = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getPointMarker(); + + $dataValues = []; + foreach ($dataValuesY as $k => $dataValueY) { + $dataValues[$k] = implode(' ', array_reverse($dataValueY)); + } + $tmp = array_shift($dataValues); + $dataValues[] = $tmp; + $tmp = array_shift($dataValuesX); + $dataValuesX[] = $tmp; + + $this->graph->SetTitles(array_reverse($dataValues)); + + $seriesPlot = new RadarPlot(array_reverse($dataValuesX)); + + $dataLabel = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotLabelByIndex($i)->getDataValue(); + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if ($radarStyle == 'filled') { + $seriesPlot->SetFillColor(self::$colourSet[self::$plotColour]); + } + $this->formatPointMarker($seriesPlot, $marker); + $seriesPlot->SetLegend($dataLabel); + + $this->graph->Add($seriesPlot); + } + } + + private function renderPlotContour($groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + + $dataValues = []; + // Loop through each data series in turn + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($i)->getDataValues(); + + $dataValues[$i] = $dataValuesX; + } + $seriesPlot = new ContourPlot($dataValues); + + $this->graph->Add($seriesPlot); + } + + private function renderPlotStock($groupID): void + { + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + $plotOrder = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotOrder(); + + $dataValues = []; + // Loop through each data series in turn and build the plot arrays + foreach ($plotOrder as $i => $v) { + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v); + if ($dataValuesX === false) { + continue; + } + $dataValuesX = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($v)->getDataValues(); + foreach ($dataValuesX as $j => $dataValueX) { + $dataValues[$plotOrder[$i]][$j] = $dataValueX; + } + } + if (empty($dataValues)) { + return; + } + + $dataValuesPlot = []; + // Flatten the plot arrays to a single dimensional array to work with jpgraph + $jMax = count($dataValues[0]); + for ($j = 0; $j < $jMax; ++$j) { + for ($i = 0; $i < $seriesCount; ++$i) { + $dataValuesPlot[] = $dataValues[$i][$j] ?? null; + } + } + + // Set the x-axis labels + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + $this->graph->xaxis->SetTickLabels($datasetLabels); + } + + $seriesPlot = new StockPlot($dataValuesPlot); + $seriesPlot->SetWidth(20); + + $this->graph->Add($seriesPlot); + } + + private function renderAreaChart($groupCount): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, true, false); + } + } + + private function renderLineChart($groupCount): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotLine($i, false, false); + } + } + + private function renderBarChart($groupCount, $dimensions = '2d'): void + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotBar($i, $dimensions); + } + } + + private function renderScatterChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, false); + } + } + + private function renderBubbleChart($groupCount): void + { + $this->renderCartesianPlotArea('linlin'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotScatter($i, true); + } + } + + private function renderPieChart($groupCount, $dimensions = '2d', $doughnut = false, $multiplePlots = false): void + { + $this->renderPiePlotArea(); + + $iLimit = ($multiplePlots) ? $groupCount : 1; + for ($groupID = 0; $groupID < $iLimit; ++$groupID) { + $exploded = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotStyle(); + $datasetLabels = []; + if ($groupID == 0) { + $labelCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex(0)->getPointCount(); + if ($labelCount > 0) { + $datasetLabels = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotCategoryByIndex(0)->getDataValues(); + $datasetLabels = $this->formatDataSetLabels($groupID, $datasetLabels); + } + } + + $seriesCount = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotSeriesCount(); + // For pie charts, we only display the first series: doughnut charts generally display all series + $jLimit = ($multiplePlots) ? $seriesCount : 1; + // Loop through each data series in turn + for ($j = 0; $j < $jLimit; ++$j) { + $dataValues = $this->chart->getPlotArea()->getPlotGroupByIndex($groupID)->getPlotValuesByIndex($j)->getDataValues(); + + // Fill in any missing values in the $dataValues array + $testCurrentIndex = 0; + foreach ($dataValues as $k => $dataValue) { + while ($k != $testCurrentIndex) { + $dataValues[$testCurrentIndex] = null; + ++$testCurrentIndex; + } + ++$testCurrentIndex; + } + + if ($dimensions == '3d') { + $seriesPlot = new PiePlot3D($dataValues); + } else { + if ($doughnut) { + $seriesPlot = new PiePlotC($dataValues); + } else { + $seriesPlot = new PiePlot($dataValues); + } + } + + if ($multiplePlots) { + $seriesPlot->SetSize(($jLimit - $j) / ($jLimit * 4)); + } + + if ($doughnut && method_exists($seriesPlot, 'SetMidColor')) { + $seriesPlot->SetMidColor('white'); + } + + $seriesPlot->SetColor(self::$colourSet[self::$plotColour++]); + if (count($datasetLabels) > 0) { + $seriesPlot->SetLabels(array_fill(0, count($datasetLabels), '')); + } + if ($dimensions != '3d') { + $seriesPlot->SetGuideLines(false); + } + if ($j == 0) { + if ($exploded) { + $seriesPlot->ExplodeAll(); + } + $seriesPlot->SetLegends($datasetLabels); + } + + $this->graph->Add($seriesPlot); + } + } + } + + private function renderRadarChart($groupCount): void + { + $this->renderRadarPlotArea(); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotRadar($groupID); + } + } + + private function renderStockChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($groupID = 0; $groupID < $groupCount; ++$groupID) { + $this->renderPlotStock($groupID); + } + } + + private function renderContourChart($groupCount): void + { + $this->renderCartesianPlotArea('intint'); + + for ($i = 0; $i < $groupCount; ++$i) { + $this->renderPlotContour($i); + } + } + + private function renderCombinationChart($groupCount, $outputDestination) + { + $this->renderCartesianPlotArea(); + + for ($i = 0; $i < $groupCount; ++$i) { + $dimensions = null; + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + switch ($chartType) { + case 'area3DChart': + case 'areaChart': + $this->renderPlotLine($i, true, true); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderPlotBar($i, $dimensions); + + break; + case 'line3DChart': + case 'lineChart': + $this->renderPlotLine($i, false, true); + + break; + case 'scatterChart': + $this->renderPlotScatter($i, false); + + break; + case 'bubbleChart': + $this->renderPlotScatter($i, true); + + break; + default: + $this->graph = null; + + return false; + } + } + + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } + + public function render($outputDestination) + { + self::$plotColour = 0; + + $groupCount = $this->chart->getPlotArea()->getPlotGroupCount(); + + $dimensions = null; + if ($groupCount == 1) { + $chartType = $this->chart->getPlotArea()->getPlotGroupByIndex(0)->getPlotType(); + } else { + $chartTypes = []; + for ($i = 0; $i < $groupCount; ++$i) { + $chartTypes[] = $this->chart->getPlotArea()->getPlotGroupByIndex($i)->getPlotType(); + } + $chartTypes = array_unique($chartTypes); + if (count($chartTypes) == 1) { + $chartType = array_pop($chartTypes); + } elseif (count($chartTypes) == 0) { + echo 'Chart is not yet implemented
'; + + return false; + } else { + return $this->renderCombinationChart($groupCount, $outputDestination); + } + } + + switch ($chartType) { + case 'area3DChart': + $dimensions = '3d'; + // no break + case 'areaChart': + $this->renderAreaChart($groupCount); + + break; + case 'bar3DChart': + $dimensions = '3d'; + // no break + case 'barChart': + $this->renderBarChart($groupCount, $dimensions); + + break; + case 'line3DChart': + $dimensions = '3d'; + // no break + case 'lineChart': + $this->renderLineChart($groupCount); + + break; + case 'pie3DChart': + $dimensions = '3d'; + // no break + case 'pieChart': + $this->renderPieChart($groupCount, $dimensions, false, false); + + break; + case 'doughnut3DChart': + $dimensions = '3d'; + // no break + case 'doughnutChart': + $this->renderPieChart($groupCount, $dimensions, true, true); + + break; + case 'scatterChart': + $this->renderScatterChart($groupCount); + + break; + case 'bubbleChart': + $this->renderBubbleChart($groupCount); + + break; + case 'radarChart': + $this->renderRadarChart($groupCount); + + break; + case 'surface3DChart': + case 'surfaceChart': + $this->renderContourChart($groupCount); + + break; + case 'stockChart': + $this->renderStockChart($groupCount); + + break; + default: + echo $chartType . ' is not yet implemented
'; + + return false; + } + $this->renderLegend(); + + $this->graph->Stroke($outputDestination); + + return true; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php new file mode 100644 index 00000000000..e1f0f90addd --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/Renderer/MtJpGraphRenderer.php @@ -0,0 +1,36 @@ +layout; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/TrendLine.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/TrendLine.php new file mode 100644 index 00000000000..75a5896c937 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Chart/TrendLine.php @@ -0,0 +1,226 @@ +setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); + } + + public function getTrendLineType(): string + { + return $this->trendLineType; + } + + public function setTrendLineType(string $trendLineType): self + { + $this->trendLineType = $trendLineType; + + return $this; + } + + public function getOrder(): int + { + return $this->order; + } + + public function setOrder(int $order): self + { + $this->order = $order; + + return $this; + } + + public function getPeriod(): int + { + return $this->period; + } + + public function setPeriod(int $period): self + { + $this->period = $period; + + return $this; + } + + public function getDispRSqr(): bool + { + return $this->dispRSqr; + } + + public function setDispRSqr(bool $dispRSqr): self + { + $this->dispRSqr = $dispRSqr; + + return $this; + } + + public function getDispEq(): bool + { + return $this->dispEq; + } + + public function setDispEq(bool $dispEq): self + { + $this->dispEq = $dispEq; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getBackward(): float + { + return $this->backward; + } + + public function setBackward(float $backward): self + { + $this->backward = $backward; + + return $this; + } + + public function getForward(): float + { + return $this->forward; + } + + public function setForward(float $forward): self + { + $this->forward = $forward; + + return $this; + } + + public function getIntercept(): float + { + return $this->intercept; + } + + public function setIntercept(float $intercept): self + { + $this->intercept = $intercept; + + return $this; + } + + public function setTrendLineProperties( + ?string $trendLineType = null, + ?int $order = 0, + ?int $period = 0, + ?bool $dispRSqr = false, + ?bool $dispEq = false, + ?float $backward = null, + ?float $forward = null, + ?float $intercept = null, + ?string $name = null + ): self { + if (!empty($trendLineType)) { + $this->setTrendLineType($trendLineType); + } + if ($order !== null) { + $this->setOrder($order); + } + if ($period !== null) { + $this->setPeriod($period); + } + if ($dispRSqr !== null) { + $this->setDispRSqr($dispRSqr); + } + if ($dispEq !== null) { + $this->setDispEq($dispEq); + } + if ($backward !== null) { + $this->setBackward($backward); + } + if ($forward !== null) { + $this->setForward($forward); + } + if ($intercept !== null) { + $this->setIntercept($intercept); + } + if ($name !== null) { + $this->setName($name); + } + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php index e3d81cb44d1..82c9ae1ae3b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Cells.php @@ -6,11 +6,14 @@ use Generator; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Psr\SimpleCache\CacheInterface; class Cells { + protected const MAX_COLUMN_ID = 16384; + /** * @var CacheInterface */ @@ -45,9 +48,10 @@ class Cells private $currentCellIsDirty = false; /** - * An index of existing cells. Booleans indexed by their coordinate. + * An index of existing cells. int pointer to the coordinate (0-base-indexed row * 16,384 + 1-base indexed column) + * indexed by their coordinate. * - * @var bool[] + * @var int[] */ private $index = []; @@ -87,27 +91,18 @@ class Cells * Whether the collection holds a cell for the given coordinate. * * @param string $cellCoordinate Coordinate of the cell to check - * - * @return bool */ - public function has($cellCoordinate) + public function has($cellCoordinate): bool { - if ($cellCoordinate === $this->currentCoordinate) { - return true; - } - - // Check if the requested entry exists in the index - return isset($this->index[$cellCoordinate]); + return ($cellCoordinate === $this->currentCoordinate) || isset($this->index[$cellCoordinate]); } /** * Add or update a cell in the collection. * * @param Cell $cell Cell to update - * - * @return Cell */ - public function update(Cell $cell) + public function update(Cell $cell): Cell { return $this->add($cell->getCoordinate(), $cell); } @@ -149,44 +144,9 @@ class Cells */ public function getSortedCoordinates() { - $sortKeys = []; - foreach ($this->getCoordinates() as $coord) { - $column = ''; - $row = 0; - sscanf($coord, '%[A-Z]%d', $column, $row); - $sortKeys[sprintf('%09d%3s', $row, $column)] = $coord; - } - ksort($sortKeys); + asort($this->index); - return array_values($sortKeys); - } - - /** - * Get highest worksheet column and highest row that have cell records. - * - * @return array Highest column name and highest row number - */ - public function getHighestRowAndColumn() - { - // Lookup highest column and highest row - $col = ['A' => '1A']; - $row = [1]; - foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - sscanf($coord, '%[A-Z]%d', $c, $r); - $row[$r] = $r; - $col[$c] = strlen($c) . $c; - } - - // Determine highest column and row - $highestRow = max($row); - $highestColumn = substr((string) @max($col), 1); - - return [ - 'row' => $highestRow, - 'column' => $highestColumn, - ]; + return array_keys($this->index); } /** @@ -201,14 +161,9 @@ class Cells /** * Return the column coordinate of the currently active cell object. - * - * @return string */ - public function getCurrentColumn() + public function getCurrentColumn(): string { - $column = ''; - $row = 0; - sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); return $column; @@ -216,19 +171,36 @@ class Cells /** * Return the row coordinate of the currently active cell object. - * - * @return int */ - public function getCurrentRow() + public function getCurrentRow(): int { - $column = ''; - $row = 0; - sscanf($this->currentCoordinate ?? '', '%[A-Z]%d', $column, $row); return (int) $row; } + /** + * Get highest worksheet column and highest row that have cell records. + * + * @return array Highest column name and highest row number + */ + public function getHighestRowAndColumn() + { + // Lookup highest column and highest row + $maxRow = $maxColumn = 1; + foreach ($this->index as $coordinate) { + $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1; + $maxRow = ($maxRow > $row) ? $maxRow : $row; + $column = $coordinate % self::MAX_COLUMN_ID; + $maxColumn = ($maxColumn > $column) ? $maxColumn : $column; + } + + return [ + 'row' => $maxRow, + 'column' => Coordinate::stringFromColumnIndex($maxColumn), + ]; + } + /** * Get highest worksheet column. * @@ -240,24 +212,26 @@ class Cells public function getHighestColumn($row = null) { if ($row === null) { - $colRow = $this->getHighestRowAndColumn(); - - return $colRow['column']; + return $this->getHighestRowAndColumn()['column']; } - $columnList = [1]; - foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; + $row = (int) $row; + if ($row <= 0) { + throw new PhpSpreadsheetException('Row number must be a positive integer'); + } - sscanf($coord, '%[A-Z]%d', $c, $r); - if ($r != $row) { + $maxColumn = 1; + $toRow = $row * self::MAX_COLUMN_ID; + $fromRow = --$row * self::MAX_COLUMN_ID; + foreach ($this->index as $coordinate) { + if ($coordinate < $fromRow || $coordinate >= $toRow) { continue; } - $columnList[] = Coordinate::columnIndexFromString($c); + $column = $coordinate % self::MAX_COLUMN_ID; + $maxColumn = $maxColumn > $column ? $maxColumn : $column; } - return Coordinate::stringFromColumnIndex((int) @max($columnList)); + return Coordinate::stringFromColumnIndex($maxColumn); } /** @@ -271,24 +245,20 @@ class Cells public function getHighestRow($column = null) { if ($column === null) { - $colRow = $this->getHighestRowAndColumn(); - - return $colRow['row']; + return $this->getHighestRowAndColumn()['row']; } - $rowList = [0]; - foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; - - sscanf($coord, '%[A-Z]%d', $c, $r); - if ($c != $column) { + $maxRow = 1; + $columnIndex = Coordinate::columnIndexFromString($column); + foreach ($this->index as $coordinate) { + if ($coordinate % self::MAX_COLUMN_ID !== $columnIndex) { continue; } - $rowList[] = $r; + $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1; + $maxRow = ($maxRow > $row) ? $maxRow : $row; } - return max($rowList); + return $maxRow; } /** @@ -298,7 +268,11 @@ class Cells */ private function getUniqueID() { - return uniqid('phpspreadsheet.', true) . '.'; + $cacheType = Settings::getCache(); + + return ($cacheType instanceof Memory\SimpleCache1 || $cacheType instanceof Memory\SimpleCache3) + ? random_bytes(7) . ':' + : uniqid('phpspreadsheet.', true) . '.'; } /** @@ -312,27 +286,18 @@ class Cells $newCollection = clone $this; $newCollection->parent = $worksheet; - if (is_object($newCollection->currentCell)) { - $newCollection->currentCell->attach($this); - } - - // Get old values - $oldKeys = $newCollection->getAllCacheKeys(); - $oldValues = $newCollection->cache->getMultiple($oldKeys); - $newValues = []; - $oldCachePrefix = $newCollection->cachePrefix; - - // Change prefix $newCollection->cachePrefix = $newCollection->getUniqueID(); - foreach ($oldValues as $oldKey => $value) { - /** @var string $newKey */ - $newKey = str_replace($oldCachePrefix, $newCollection->cachePrefix, $oldKey); - $newValues[$newKey] = clone $value; - } - // Store new values - $stored = $newCollection->cache->setMultiple($newValues); - $this->destructIfNeeded($stored, $newCollection, 'Failed to copy cells in cache'); + foreach ($this->index as $key => $value) { + $newCollection->index[$key] = $value; + $stored = $newCollection->cache->set( + $newCollection->cachePrefix . $key, + clone $this->cache->get($this->cachePrefix . $key) + ); + if ($stored === false) { + $this->destructIfNeeded($newCollection, 'Failed to copy cells in cache'); + } + } return $newCollection; } @@ -340,17 +305,23 @@ class Cells /** * Remove a row, deleting all cells in that row. * - * @param string $row Row number to remove + * @param int|string $row Row number to remove */ public function removeRow($row): void { - foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; + $this->storeCurrentCell(); + $row = (int) $row; + if ($row <= 0) { + throw new PhpSpreadsheetException('Row number must be a positive integer'); + } - sscanf($coord, '%[A-Z]%d', $c, $r); - if ($r == $row) { - $this->delete($coord); + $toRow = $row * self::MAX_COLUMN_ID; + $fromRow = --$row * self::MAX_COLUMN_ID; + foreach ($this->index as $coordinate) { + if ($coordinate >= $fromRow && $coordinate < $toRow) { + $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1; + $column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID); + $this->delete("{$column}{$row}"); } } } @@ -362,13 +333,14 @@ class Cells */ public function removeColumn($column): void { - foreach ($this->getCoordinates() as $coord) { - $c = ''; - $r = 0; + $this->storeCurrentCell(); - sscanf($coord, '%[A-Z]%d', $c, $r); - if ($c == $column) { - $this->delete($coord); + $columnIndex = Coordinate::columnIndexFromString($column); + foreach ($this->index as $coordinate) { + if ($coordinate % self::MAX_COLUMN_ID === $columnIndex) { + $row = (int) floor($coordinate / self::MAX_COLUMN_ID) + 1; + $column = Coordinate::stringFromColumnIndex($coordinate % self::MAX_COLUMN_ID); + $this->delete("{$column}{$row}"); } } } @@ -383,7 +355,9 @@ class Cells $this->currentCell->detach(); $stored = $this->cache->set($this->cachePrefix . $this->currentCoordinate, $this->currentCell); - $this->destructIfNeeded($stored, $this, "Failed to store cell {$this->currentCoordinate} in cache"); + if ($stored === false) { + $this->destructIfNeeded($this, "Failed to store cell {$this->currentCoordinate} in cache"); + } $this->currentCellIsDirty = false; } @@ -391,13 +365,11 @@ class Cells $this->currentCell = null; } - private function destructIfNeeded(bool $stored, self $cells, string $message): void + private function destructIfNeeded(self $cells, string $message): void { - if (!$stored) { - $cells->__destruct(); + $cells->__destruct(); - throw new PhpSpreadsheetException($message); - } + throw new PhpSpreadsheetException($message); } /** @@ -413,7 +385,8 @@ class Cells if ($cellCoordinate !== $this->currentCoordinate) { $this->storeCurrentCell(); } - $this->index[$cellCoordinate] = true; + sscanf($cellCoordinate, '%[A-Z]%d', $column, $row); + $this->index[$cellCoordinate] = (--$row * self::MAX_COLUMN_ID) + Coordinate::columnIndexFromString($column); $this->currentCoordinate = $cellCoordinate; $this->currentCell = $cell; @@ -437,11 +410,11 @@ class Cells $this->storeCurrentCell(); // Return null if requested entry doesn't exist in collection - if (!$this->has($cellCoordinate)) { + if ($this->has($cellCoordinate) === false) { return null; } - // Check if the entry that has been requested actually exists + // Check if the entry that has been requested actually exists in the cache $cell = $this->cache->get($this->cachePrefix . $cellCoordinate); if ($cell === null) { throw new PhpSpreadsheetException("Cell entry {$cellCoordinate} no longer exists in cache. This probably means that the cache was cleared by someone else."); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/CellsFactory.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/CellsFactory.php index 26f18dfc52c..b3833bd8bc6 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/CellsFactory.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/CellsFactory.php @@ -12,9 +12,8 @@ abstract class CellsFactory * * @param Worksheet $worksheet Enable cell caching for this worksheet * - * @return Cells * */ - public static function getInstance(Worksheet $worksheet) + public static function getInstance(Worksheet $worksheet): Cells { return new Cells($worksheet, Settings::getCache()); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php similarity index 93% rename from lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory.php rename to lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php index 2690ab7d025..a0eb6ec2211 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Collection/Memory/SimpleCache1.php @@ -1,6 +1,6 @@ cache = []; + + return true; + } + + /** + * @param string $key + */ + public function delete($key): bool + { + unset($this->cache[$key]); + + return true; + } + + /** + * @param iterable $keys + */ + public function deleteMultiple($keys): bool + { + foreach ($keys as $key) { + $this->delete($key); + } + + return true; + } + + /** + * @param string $key + * @param mixed $default + */ + public function get($key, $default = null): mixed + { + if ($this->has($key)) { + return $this->cache[$key]; + } + + return $default; + } + + /** + * @param iterable $keys + * @param mixed $default + */ + public function getMultiple($keys, $default = null): iterable + { + $results = []; + foreach ($keys as $key) { + $results[$key] = $this->get($key, $default); + } + + return $results; + } + + /** + * @param string $key + */ + public function has($key): bool + { + return array_key_exists($key, $this->cache); + } + + /** + * @param string $key + * @param mixed $value + * @param null|DateInterval|int $ttl + */ + public function set($key, $value, $ttl = null): bool + { + $this->cache[$key] = $value; + + return true; + } + + /** + * @param iterable $values + * @param null|DateInterval|int $ttl + */ + public function setMultiple($values, $ttl = null): bool + { + foreach ($values as $key => $value) { + $this->set($key, $value); + } + + return true; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/DefinedName.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/DefinedName.php index 3b874b435b7..464fa8e3466 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/DefinedName.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/DefinedName.php @@ -150,7 +150,7 @@ abstract class DefinedName // New title $newTitle = $this->name; - ReferenceHelper::getInstance()->updateNamedFormulas($this->worksheet->getParent(), $oldTitle, $newTitle); + ReferenceHelper::getInstance()->updateNamedFormulae($this->worksheet->getParent(), $oldTitle, $newTitle); } return $this; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php index 3be5a67a855..afdeea99624 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Document/Properties.php @@ -115,7 +115,7 @@ class Properties // Initialise values $this->lastModifiedBy = $this->creator; $this->created = self::intOrFloatTimestamp(null); - $this->modified = self::intOrFloatTimestamp(null); + $this->modified = $this->created; } /** @@ -171,9 +171,9 @@ class Properties if (is_numeric($timestamp)) { $timestamp = (float) $timestamp; } else { - $timestamp = preg_replace('/[.][0-9]*$/', '', $timestamp) ?? ''; - $timestamp = preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp) ?? ''; - $timestamp = preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp) ?? ''; + $timestamp = (string) preg_replace('/[.][0-9]*$/', '', $timestamp); + $timestamp = (string) preg_replace('/^(\\d{4})- (\\d)/', '$1-0$2', $timestamp); + $timestamp = (string) preg_replace('/^(\\d{4}-\\d{2})- (\\d)/', '$1-0$2', $timestamp); $timestamp = (float) (new DateTime($timestamp))->format('U'); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Dimension.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Dimension.php index 136ffd7fbfd..ff07ce5b8fa 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Dimension.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Dimension.php @@ -55,10 +55,22 @@ class Dimension */ protected $unit; + /** + * Phpstan bug has been fixed; this function allows us to + * pass Phpstan whether fixed or not. + * + * @param mixed $value + */ + private static function stanBugFixed($value): array + { + return is_array($value) ? $value : [null, null]; + } + public function __construct(string $dimension) { - [$size, $unit] = sscanf($dimension, '%[1234567890.]%s'); - $unit = strtolower(trim($unit)); + [$size, $unit] = self::stanBugFixed(sscanf($dimension, '%[1234567890.]%s')); + $unit = strtolower(trim($unit ?? '')); + $size = (float) $size; // If a UoM is specified, then convert the size to pixels for internal storage if (isset(self::ABSOLUTE_UNITS[$unit])) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Html.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Html.php index 73a3308c249..632efebc9a3 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Html.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Html.php @@ -619,6 +619,7 @@ class Html // Load the HTML file into the DOM object // Note the use of error suppression, because typically this will be an html fragment, so not fully valid markup $prefix = ''; + /** @scrutinizer ignore-unhandled */ @$dom->loadHTML($prefix . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); // Discard excess white space $dom->preserveWhiteSpace = false; @@ -641,7 +642,7 @@ class Html $text = ltrim($text); } // Trim any spaces immediately after a line break - $text = preg_replace('/\n */mu', "\n", $text); + $text = (string) preg_replace('/\n */mu', "\n", $text); $element->setText($text); } } @@ -791,10 +792,10 @@ class Html protected function parseTextNode(DOMText $textNode): void { - $domText = preg_replace( + $domText = (string) preg_replace( '/\s+/u', ' ', - str_replace(["\r", "\n"], ' ', $textNode->nodeValue) + str_replace(["\r", "\n"], ' ', $textNode->nodeValue ?? '') ); $this->stringData .= $domText; $this->buildTextRun(); @@ -808,6 +809,7 @@ class Html if (isset($callbacks[$callbackTag])) { $elementHandler = $callbacks[$callbackTag]; if (method_exists($this, $elementHandler)) { + /** @phpstan-ignore-next-line */ call_user_func([$this, $elementHandler], $element); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php index 257a02a8273..0ac0c796911 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/Sample.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Helper; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\IWriter; use RecursiveDirectoryIterator; use RecursiveIteratorIterator; @@ -84,8 +85,8 @@ class Sample foreach ($regex as $file) { $file = str_replace(str_replace('\\', '/', $baseDir) . '/', '', str_replace('\\', '/', $file[0])); $info = pathinfo($file); - $category = str_replace('_', ' ', $info['dirname']); - $name = str_replace('_', ' ', preg_replace('/(|\.php)/', '', $info['filename'])); + $category = str_replace('_', ' ', $info['dirname'] ?? ''); + $name = str_replace('_', ' ', (string) preg_replace('/(|\.php)/', '', $info['filename'])); if (!in_array($category, ['.', 'boostrap', 'templates'])) { if (!isset($files[$category])) { $files[$category] = []; @@ -182,6 +183,33 @@ class Sample echo date('H:i:s ') . $message . $eol; } + public function titles(string $category, string $functionName, ?string $description = null): void + { + $this->log(sprintf('%s Functions:', $category)); + $description === null + ? $this->log(sprintf('Function: %s()', rtrim($functionName, '()'))) + : $this->log(sprintf('Function: %s() - %s.', rtrim($functionName, '()'), rtrim($description, '.'))); + } + + public function displayGrid(array $matrix): void + { + $renderer = new TextGrid($matrix, $this->isCli()); + echo $renderer->render(); + } + + public function logCalculationResult( + Worksheet $worksheet, + string $functionName, + string $formulaCell, + ?string $descriptionCell = null + ): void { + if ($descriptionCell !== null) { + $this->log($worksheet->getCell($descriptionCell)->getValue()); + } + $this->log($worksheet->getCell($formulaCell)->getValue()); + $this->log(sprintf('%s() Result is ', $functionName) . $worksheet->getCell($formulaCell)->getCalculatedValue()); + } + /** * Log ending notes. */ diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php new file mode 100644 index 00000000000..acb9ae60ea4 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Helper/TextGrid.php @@ -0,0 +1,139 @@ +rows = array_keys($matrix); + $this->columns = array_keys($matrix[$this->rows[0]]); + + $matrix = array_values($matrix); + array_walk( + $matrix, + function (&$row): void { + $row = array_values($row); + } + ); + + $this->matrix = $matrix; + $this->isCli = $isCli; + } + + public function render(): string + { + $this->gridDisplay = $this->isCli ? '' : ''; + + $maxRow = max($this->rows); + $maxRowLength = strlen((string) $maxRow) + 1; + $columnWidths = $this->getColumnWidths($this->matrix); + + $this->renderColumnHeader($maxRowLength, $columnWidths); + $this->renderRows($maxRowLength, $columnWidths); + $this->renderFooter($maxRowLength, $columnWidths); + + $this->gridDisplay .= $this->isCli ? '' : ''; + + return $this->gridDisplay; + } + + private function renderRows(int $maxRowLength, array $columnWidths): void + { + foreach ($this->matrix as $row => $rowData) { + $this->gridDisplay .= '|' . str_pad((string) $this->rows[$row], $maxRowLength, ' ', STR_PAD_LEFT) . ' '; + $this->renderCells($rowData, $columnWidths); + $this->gridDisplay .= '|' . PHP_EOL; + } + } + + private function renderCells(array $rowData, array $columnWidths): void + { + foreach ($rowData as $column => $cell) { + $cell = ($this->isCli) ? (string) $cell : htmlentities((string) $cell); + $this->gridDisplay .= '| '; + $this->gridDisplay .= str_pad($cell, $columnWidths[$column] + 1, ' '); + } + } + + private function renderColumnHeader(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-' . str_repeat('-', $columnWidths[$column] + 1); + } + $this->gridDisplay .= '+' . PHP_EOL; + + $this->gridDisplay .= str_repeat(' ', $maxRowLength + 2); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '| ' . str_pad((string) $reference, $columnWidths[$column] + 1, ' '); + } + $this->gridDisplay .= '|' . PHP_EOL; + + $this->renderFooter($maxRowLength, $columnWidths); + } + + private function renderFooter(int $maxRowLength, array $columnWidths): void + { + $this->gridDisplay .= '+' . str_repeat('-', $maxRowLength + 1); + foreach ($this->columns as $column => $reference) { + $this->gridDisplay .= '+-'; + $this->gridDisplay .= str_pad((string) '', $columnWidths[$column] + 1, '-'); + } + $this->gridDisplay .= '+' . PHP_EOL; + } + + private function getColumnWidths(array $matrix): array + { + $columnCount = count($this->matrix, COUNT_RECURSIVE) / count($this->matrix); + $columnWidths = []; + for ($column = 0; $column < $columnCount; ++$column) { + $columnWidths[] = $this->getColumnWidth(array_column($this->matrix, $column)); + } + + return $columnWidths; + } + + private function getColumnWidth(array $columnData): int + { + $columnWidth = 0; + $columnData = array_values($columnData); + + foreach ($columnData as $columnValue) { + if (is_string($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue)); + } elseif (is_bool($columnValue)) { + $columnWidth = max($columnWidth, strlen($columnValue ? 'TRUE' : 'FALSE')); + } + + $columnWidth = max($columnWidth, strlen((string) $columnWidth)); + } + + return $columnWidth; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php index 91613cb440d..e437a220651 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/IOFactory.php @@ -14,23 +14,39 @@ use PhpOffice\PhpSpreadsheet\Writer\IWriter; */ abstract class IOFactory { + public const READER_XLSX = 'Xlsx'; + public const READER_XLS = 'Xls'; + public const READER_XML = 'Xml'; + public const READER_ODS = 'Ods'; + public const READER_SYLK = 'Slk'; + public const READER_SLK = 'Slk'; + public const READER_GNUMERIC = 'Gnumeric'; + public const READER_HTML = 'Html'; + public const READER_CSV = 'Csv'; + + public const WRITER_XLSX = 'Xlsx'; + public const WRITER_XLS = 'Xls'; + public const WRITER_ODS = 'Ods'; + public const WRITER_CSV = 'Csv'; + public const WRITER_HTML = 'Html'; + private static $readers = [ - 'Xlsx' => Reader\Xlsx::class, - 'Xls' => Reader\Xls::class, - 'Xml' => Reader\Xml::class, - 'Ods' => Reader\Ods::class, - 'Slk' => Reader\Slk::class, - 'Gnumeric' => Reader\Gnumeric::class, - 'Html' => Reader\Html::class, - 'Csv' => Reader\Csv::class, + self::READER_XLSX => Reader\Xlsx::class, + self::READER_XLS => Reader\Xls::class, + self::READER_XML => Reader\Xml::class, + self::READER_ODS => Reader\Ods::class, + self::READER_SLK => Reader\Slk::class, + self::READER_GNUMERIC => Reader\Gnumeric::class, + self::READER_HTML => Reader\Html::class, + self::READER_CSV => Reader\Csv::class, ]; private static $writers = [ - 'Xls' => Writer\Xls::class, - 'Xlsx' => Writer\Xlsx::class, - 'Ods' => Writer\Ods::class, - 'Csv' => Writer\Csv::class, - 'Html' => Writer\Html::class, + self::WRITER_XLS => Writer\Xls::class, + self::WRITER_XLSX => Writer\Xlsx::class, + self::WRITER_ODS => Writer\Ods::class, + self::WRITER_CSV => Writer\Csv::class, + self::WRITER_HTML => Writer\Html::class, 'Tcpdf' => Writer\Pdf\Tcpdf::class, 'Dompdf' => Writer\Pdf\Dompdf::class, 'Mpdf' => Writer\Pdf\Mpdf::class, @@ -70,10 +86,18 @@ abstract class IOFactory * Loads Spreadsheet from file using automatic Reader\IReader resolution. * * @param string $filename The name of the spreadsheet file + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. */ - public static function load(string $filename, int $flags = 0): Spreadsheet + public static function load(string $filename, int $flags = 0, ?array $readers = null): Spreadsheet { - $reader = self::createReaderForFile($filename); + $reader = self::createReaderForFile($filename, $readers); return $reader->load($filename, $flags); } @@ -81,9 +105,9 @@ abstract class IOFactory /** * Identify file type using automatic IReader resolution. */ - public static function identify(string $filename): string + public static function identify(string $filename, ?array $readers = null): string { - $reader = self::createReaderForFile($filename); + $reader = self::createReaderForFile($filename, $readers); $className = get_class($reader); $classType = explode('\\', $className); unset($reader); @@ -93,14 +117,32 @@ abstract class IOFactory /** * Create Reader\IReader for file using automatic IReader resolution. + * + * @param string[] $readers An array of Readers to use to identify the file type. By default, load() will try + * all possible Readers until it finds a match; but this allows you to pass in a + * list of Readers so it will only try the subset that you specify here. + * Values in this list can be any of the constant values defined in the set + * IOFactory::READER_*. */ - public static function createReaderForFile(string $filename): IReader + public static function createReaderForFile(string $filename, ?array $readers = null): IReader { File::assertFile($filename); + $testReaders = self::$readers; + if ($readers !== null) { + $readers = array_map('strtoupper', $readers); + $testReaders = array_filter( + self::$readers, + function (string $readerType) use ($readers) { + return in_array(strtoupper($readerType), $readers, true); + }, + ARRAY_FILTER_USE_KEY + ); + } + // First, lucky guess by inspecting file extension $guessedReader = self::getReaderTypeFromExtension($filename); - if ($guessedReader !== null) { + if (($guessedReader !== null) && array_key_exists($guessedReader, $testReaders)) { $reader = self::createReader($guessedReader); // Let's see if we are lucky @@ -110,11 +152,11 @@ abstract class IOFactory } // If we reach here then "lucky guess" didn't give any result - // Try walking through all the options in self::$autoResolveClasses - foreach (self::$readers as $type => $class) { + // Try walking through all the options in self::$readers (or the selected subset) + foreach ($testReaders as $readerType => $class) { // Ignore our original guess, we know that won't work - if ($type !== $guessedReader) { - $reader = self::createReader($type); + if ($readerType !== $guessedReader) { + $reader = self::createReader($readerType); if ($reader->canRead($filename)) { return $reader; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php index 2ad8e6b20aa..a137e78cbf6 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/BaseReader.php @@ -2,9 +2,11 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; use PhpOffice\PhpSpreadsheet\Shared\File; +use PhpOffice\PhpSpreadsheet\Spreadsheet; abstract class BaseReader implements IReader { @@ -144,25 +146,45 @@ abstract class BaseReader implements IReader } } + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { + throw new PhpSpreadsheetException('Reader classes must implement their own loadSpreadsheetFromFile() method'); + } + + /** + * Loads Spreadsheet from file. + * + * @param int $flags the optional second parameter flags may be used to identify specific elements + * that should be loaded, but which won't be loaded by default, using these values: + * IReader::LOAD_WITH_CHARTS - Include any charts that are defined in the loaded file + */ + public function load(string $filename, int $flags = 0): Spreadsheet + { + $this->processFlags($flags); + + try { + return $this->loadSpreadsheetFromFile($filename); + } catch (ReaderException $e) { + throw $e; + } + } + /** * Open file for reading. - * - * @param string $filename */ - protected function openFile($filename): void + protected function openFile(string $filename): void { + $fileHandle = false; if ($filename) { File::assertFile($filename); // Open file $fileHandle = fopen($filename, 'rb'); - } else { - $fileHandle = false; } - if ($fileHandle !== false) { - $this->fileHandle = $fileHandle; - } else { + if ($fileHandle === false) { throw new ReaderException('Could not open file ' . $filename . ' for reading.'); } + + $this->fileHandle = $fileHandle; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php index 185f064c3f3..65a71edb03e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv.php @@ -2,12 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Reader\Csv\Delimiter; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat; class Csv extends BaseReader { @@ -84,6 +86,26 @@ class Csv extends BaseReader */ private static $constructorCallback; + /** + * Attempt autodetect line endings (deprecated after PHP8.1)? + * + * @var bool + */ + private $testAutodetect = true; + + /** + * @var bool + */ + protected $castFormattedNumberToNumeric = false; + + /** + * @var bool + */ + protected $preserveNumericFormatting = false; + + /** @var bool */ + private $preserveNullString = false; + /** * Create a new CSV Reader instance. */ @@ -236,13 +258,9 @@ class Csv extends BaseReader /** * Loads Spreadsheet from file. - * - * @return Spreadsheet */ - public function load(string $filename, int $flags = 0) + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { - $this->processFlags($flags); - // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -250,6 +268,18 @@ class Csv extends BaseReader return $this->loadIntoExisting($filename, $spreadsheet); } + /** + * Loads Spreadsheet from string. + */ + public function loadSpreadsheetFromString(string $contents): Spreadsheet + { + // Create new Spreadsheet + $spreadsheet = new Spreadsheet(); + + // Load into this instance + return $this->loadStringOrFile('data://text/plain,' . urlencode($contents), $spreadsheet, true); + } + private function openFileOrMemory(string $filename): void { // Open file @@ -273,10 +303,17 @@ class Csv extends BaseReader } } - private static function setAutoDetect(?string $value): ?string + public function setTestAutoDetect(bool $value): self + { + $this->testAutodetect = $value; + + return $this; + } + + private function setAutoDetect(?string $value): ?string { $retVal = null; - if ($value !== null) { + if ($value !== null && $this->testAutodetect) { $retVal2 = @ini_set('auto_detect_line_endings', $value); if (is_string($retVal2)) { $retVal = $retVal2; @@ -286,16 +323,51 @@ class Csv extends BaseReader return $retVal; } + public function castFormattedNumberToNumeric( + bool $castFormattedNumberToNumeric, + bool $preserveNumericFormatting = false + ): void { + $this->castFormattedNumberToNumeric = $castFormattedNumberToNumeric; + $this->preserveNumericFormatting = $preserveNumericFormatting; + } + + /** + * Open data uri for reading. + */ + private function openDataUri(string $filename): void + { + $fileHandle = fopen($filename, 'rb'); + if ($fileHandle === false) { + // @codeCoverageIgnoreStart + throw new ReaderException('Could not open file ' . $filename . ' for reading.'); + // @codeCoverageIgnoreEnd + } + + $this->fileHandle = $fileHandle; + } + /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. */ public function loadIntoExisting(string $filename, Spreadsheet $spreadsheet): Spreadsheet + { + return $this->loadStringOrFile($filename, $spreadsheet, false); + } + + /** + * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. + */ + private function loadStringOrFile(string $filename, Spreadsheet $spreadsheet, bool $dataUri): Spreadsheet { // Deprecated in Php8.1 - $iniset = self::setAutoDetect('1'); + $iniset = $this->setAutoDetect('1'); // Open file - $this->openFileOrMemory($filename); + if ($dataUri) { + $this->openDataUri($filename); + } else { + $this->openFileOrMemory($filename); + } $fileHandle = $this->fileHandle; // Skip BOM, if any @@ -322,7 +394,8 @@ class Csv extends BaseReader $columnLetter = 'A'; foreach ($rowData as $rowDatum) { $this->convertBoolean($rowDatum, $preserveBooleanString); - if ($rowDatum !== '' && $this->readFilter->readCell($columnLetter, $currentRow)) { + $numberFormatMask = $this->convertFormattedNumber($rowDatum); + if (($rowDatum !== '' || $this->preserveNullString) && $this->readFilter->readCell($columnLetter, $currentRow)) { if ($this->contiguous) { if ($noOutputYet) { $noOutputYet = false; @@ -331,6 +404,10 @@ class Csv extends BaseReader } else { $outRow = $currentRow; } + // Set basic styling for the value (Note that this could be overloaded by styling in a value binder) + $sheet->getCell($columnLetter . $outRow)->getStyle() + ->getNumberFormat() + ->setFormatCode($numberFormatMask); // Set cell value $sheet->getCell($columnLetter . $outRow)->setValue($rowDatum); } @@ -343,7 +420,7 @@ class Csv extends BaseReader // Close file fclose($fileHandle); - self::setAutoDetect($iniset); + $this->setAutoDetect($iniset); // Return return $spreadsheet; @@ -357,16 +434,49 @@ class Csv extends BaseReader private function convertBoolean(&$rowDatum, bool $preserveBooleanString): void { if (is_string($rowDatum) && !$preserveBooleanString) { - if (strcasecmp('true', $rowDatum) === 0) { + if (strcasecmp(Calculation::getTRUE(), $rowDatum) === 0 || strcasecmp('true', $rowDatum) === 0) { $rowDatum = true; - } elseif (strcasecmp('false', $rowDatum) === 0) { + } elseif (strcasecmp(Calculation::getFALSE(), $rowDatum) === 0 || strcasecmp('false', $rowDatum) === 0) { $rowDatum = false; } - } elseif ($rowDatum === null) { - $rowDatum = ''; + } else { + $rowDatum = $rowDatum ?? ''; } } + /** + * Convert numeric strings to int or float values. + * + * @param mixed $rowDatum + */ + private function convertFormattedNumber(&$rowDatum): string + { + $numberFormatMask = NumberFormat::FORMAT_GENERAL; + if ($this->castFormattedNumberToNumeric === true && is_string($rowDatum)) { + $numeric = str_replace( + [StringHelper::getThousandsSeparator(), StringHelper::getDecimalSeparator()], + ['', '.'], + $rowDatum + ); + + if (is_numeric($numeric)) { + $decimalPos = strpos($rowDatum, StringHelper::getDecimalSeparator()); + if ($this->preserveNumericFormatting === true) { + $numberFormatMask = (strpos($rowDatum, StringHelper::getThousandsSeparator()) !== false) + ? '#,##0' : '0'; + if ($decimalPos !== false) { + $decimals = strlen($rowDatum) - $decimalPos - 1; + $numberFormatMask .= '.' . str_repeat('0', min($decimals, 6)); + } + } + + $rowDatum = ($decimalPos !== false) ? (float) $numeric : (int) $numeric; + } + } + + return $numberFormatMask; + } + public function getDelimiter(): ?string { return $this->delimiter; @@ -520,4 +630,16 @@ class Csv extends BaseReader return ($encoding === '') ? $dflt : $encoding; } + + public function setPreserveNullString(bool $value): self + { + $this->preserveNullString = $value; + + return $this; + } + + public function getPreserveNullString(): bool + { + return $this->preserveNullString; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php index fc298957b83..029d4a186ec 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Csv/Delimiter.php @@ -140,12 +140,12 @@ class Delimiter $line = $line . $newLine; // Drop everything that is enclosed to avoid counting false positives in enclosures - $line = preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line); + $line = (string) preg_replace('/(' . $enclosure . '.*' . $enclosure . ')/Us', '', $line); // See if we have any enclosures left in the line // if we still have an enclosure then we need to read the next line as well - } while (preg_match('/(' . $enclosure . ')/', $line ?? '') > 0); + } while (preg_match('/(' . $enclosure . ')/', $line) > 0); - return $line ?? false; + return ($line !== '') ? $line : false; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php index 9b03cdc9a80..1dcb0a125a4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -227,13 +227,9 @@ class Gnumeric extends BaseReader /** * Loads Spreadsheet from file. - * - * @return Spreadsheet */ - public function load(string $filename, int $flags = 0) + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { - $this->processFlags($flags); - // Create new Spreadsheet $spreadsheet = new Spreadsheet(); $spreadsheet->removeSheetByIndex(0); @@ -276,6 +272,11 @@ class Gnumeric extends BaseReader // name in line with the formula, not the reverse $this->spreadsheet->getActiveSheet()->setTitle($worksheetName, false, false); + $visibility = $sheetOrNull->attributes()['Visibility'] ?? 'GNM_SHEET_VISIBILITY_VISIBLE'; + if ((string) $visibility !== 'GNM_SHEET_VISIBILITY_VISIBLE') { + $this->spreadsheet->getActiveSheet()->setSheetState(Worksheet::SHEETSTATE_HIDDEN); + } + if (!$this->readDataOnly) { (new PageSetup($this->spreadsheet)) ->printInformation($sheet) @@ -288,12 +289,8 @@ class Gnumeric extends BaseReader $row = (int) $cellAttributes->Row + 1; $column = (int) $cellAttributes->Col; - if ($row > $maxRow) { - $maxRow = $row; - } - if ($column > $maxCol) { - $maxCol = $column; - } + $maxRow = max($maxRow, $row); + $maxCol = max($maxCol, $column); $column = Coordinate::stringFromColumnIndex($column + 1); @@ -304,38 +301,7 @@ class Gnumeric extends BaseReader } } - $ValueType = $cellAttributes->ValueType; - $ExprID = (string) $cellAttributes->ExprID; - $type = DataType::TYPE_FORMULA; - if ($ExprID > '') { - if (((string) $cell) > '') { - $this->expressions[$ExprID] = [ - 'column' => $cellAttributes->Col, - 'row' => $cellAttributes->Row, - 'formula' => (string) $cell, - ]; - } else { - $expression = $this->expressions[$ExprID]; - - $cell = $this->referenceHelper->updateFormulaReferences( - $expression['formula'], - 'A1', - $cellAttributes->Col - $expression['column'], - $cellAttributes->Row - $expression['row'], - $worksheetName - ); - } - $type = DataType::TYPE_FORMULA; - } else { - $vtype = (string) $ValueType; - if (array_key_exists($vtype, self::$mappings['dataType'])) { - $type = self::$mappings['dataType'][$vtype]; - } - if ($vtype === '20') { // Boolean - $cell = $cell == 'TRUE'; - } - } - $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); + $this->loadCell($cell, $worksheetName, $cellAttributes, $column, $row); } if ($sheet->Styles !== null) { @@ -348,22 +314,56 @@ class Gnumeric extends BaseReader $this->processMergedCells($sheet); $this->processAutofilter($sheet); + $this->setSelectedCells($sheet); ++$worksheetID; } $this->processDefinedNames($gnmXML); + $this->setSelectedSheet($gnmXML); + // Return return $this->spreadsheet; } + private function setSelectedSheet(SimpleXMLElement $gnmXML): void + { + if (isset($gnmXML->UIData)) { + $attributes = self::testSimpleXml($gnmXML->UIData->attributes()); + $selectedSheet = (int) $attributes['SelectedTab']; + $this->spreadsheet->setActiveSheetIndex($selectedSheet); + } + } + + private function setSelectedCells(?SimpleXMLElement $sheet): void + { + if ($sheet !== null && isset($sheet->Selections)) { + foreach ($sheet->Selections as $selection) { + $startCol = (int) ($selection->StartCol ?? 0); + $startRow = (int) ($selection->StartRow ?? 0) + 1; + $endCol = (int) ($selection->EndCol ?? $startCol); + $endRow = (int) ($selection->endRow ?? 0) + 1; + + $startColumn = Coordinate::stringFromColumnIndex($startCol + 1); + $endColumn = Coordinate::stringFromColumnIndex($endCol + 1); + + $startCell = "{$startColumn}{$startRow}"; + $endCell = "{$endColumn}{$endRow}"; + $selectedRange = $startCell . (($endCell !== $startCell) ? ':' . $endCell : ''); + $this->spreadsheet->getActiveSheet()->setSelectedCell($selectedRange); + + break; + } + } + } + private function processMergedCells(?SimpleXMLElement $sheet): void { // Handle Merged Cells in this worksheet if ($sheet !== null && isset($sheet->MergedRegions)) { foreach ($sheet->MergedRegions->Merge as $mergeCells) { if (strpos((string) $mergeCells, ':') !== false) { - $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells); + $this->spreadsheet->getActiveSheet()->mergeCells($mergeCells, Worksheet::MERGE_CELL_CONTENT_HIDE); } } } @@ -385,7 +385,8 @@ class Gnumeric extends BaseReader private function setColumnWidth(int $whichColumn, float $defaultWidth): void { - $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + $columnDimension = $this->spreadsheet->getActiveSheet() + ->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); if ($columnDimension !== null) { $columnDimension->setWidth($defaultWidth); } @@ -393,7 +394,8 @@ class Gnumeric extends BaseReader private function setColumnInvisible(int $whichColumn): void { - $columnDimension = $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); + $columnDimension = $this->spreadsheet->getActiveSheet() + ->getColumnDimension(Coordinate::stringFromColumnIndex($whichColumn + 1)); if ($columnDimension !== null) { $columnDimension->setVisible(false); } @@ -534,4 +536,51 @@ class Gnumeric extends BaseReader return $value; } + + private function loadCell( + SimpleXMLElement $cell, + string $worksheetName, + SimpleXMLElement $cellAttributes, + string $column, + int $row + ): void { + $ValueType = $cellAttributes->ValueType; + $ExprID = (string) $cellAttributes->ExprID; + $type = DataType::TYPE_FORMULA; + if ($ExprID > '') { + if (((string) $cell) > '') { + $this->expressions[$ExprID] = [ + 'column' => $cellAttributes->Col, + 'row' => $cellAttributes->Row, + 'formula' => (string) $cell, + ]; + } else { + $expression = $this->expressions[$ExprID]; + + $cell = $this->referenceHelper->updateFormulaReferences( + $expression['formula'], + 'A1', + $cellAttributes->Col - $expression['column'], + $cellAttributes->Row - $expression['row'], + $worksheetName + ); + } + $type = DataType::TYPE_FORMULA; + } else { + $vtype = (string) $ValueType; + if (array_key_exists($vtype, self::$mappings['dataType'])) { + $type = self::$mappings['dataType'][$vtype]; + } + if ($vtype === '20') { // Boolean + $cell = $cell == 'TRUE'; + } + } + + $this->spreadsheet->getActiveSheet()->getCell($column . $row)->setValueExplicit((string) $cell, $type); + if (isset($cellAttributes->ValueFormat)) { + $this->spreadsheet->getActiveSheet()->getCell($column . $row) + ->getStyle()->getNumberFormat() + ->setFormatCode((string) $cellAttributes->ValueFormat); + } + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php index 5b501e0fa7c..204c73025df 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Gnumeric/PageSetup.php @@ -22,11 +22,8 @@ class PageSetup public function printInformation(SimpleXMLElement $sheet): self { - if (isset($sheet->PrintInformation)) { + if (isset($sheet->PrintInformation, $sheet->PrintInformation[0])) { $printInformation = $sheet->PrintInformation[0]; - if (!$printInformation) { - return $this; - } $scale = (string) $printInformation->Scale->attributes()['percentage']; $pageOrder = (string) $printInformation->order; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php index c37f1c1af18..76f128e025c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Html.php @@ -19,7 +19,6 @@ use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; -/** PhpSpreadsheet root directory */ class Html extends BaseReader { /** @@ -201,13 +200,9 @@ class Html extends BaseReader /** * Loads Spreadsheet from file. - * - * @return Spreadsheet */ - public function load(string $filename, int $flags = 0) + public function loadSpreadsheetFromFile(string $filename): Spreadsheet { - $this->processFlags($flags); - // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -624,7 +619,7 @@ class Html extends BaseReader { foreach ($element->childNodes as $child) { if ($child instanceof DOMText) { - $domText = preg_replace('/\s+/u', ' ', trim($child->nodeValue)); + $domText = (string) preg_replace('/\s+/u', ' ', trim($child->nodeValue ?? '')); if (is_string($cellContent)) { // simply append the text if the cell content is a plain text string $cellContent .= $domText; @@ -637,16 +632,6 @@ class Html extends BaseReader } } - /** - * Make sure mb_convert_encoding returns string. - * - * @param mixed $result - */ - private static function ensureString($result): string - { - return is_string($result) ? $result : ''; - } - /** * Loads PhpSpreadsheet from file into PhpSpreadsheet instance. * @@ -665,8 +650,14 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scanFile($filename), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scanFile($filename); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } @@ -677,6 +668,11 @@ class Html extends BaseReader return $this->loadDocument($dom, $spreadsheet); } + private static function replaceNonAscii(array $matches): string + { + return '&#' . mb_ord($matches[0], 'UTF-8') . ';'; + } + /** * Spreadsheet from content. * @@ -688,8 +684,14 @@ class Html extends BaseReader $dom = new DOMDocument(); // Reload the HTML file into the DOM object try { - $convert = mb_convert_encoding($this->securityScanner->scan($content), 'HTML-ENTITIES', 'UTF-8'); - $loaded = $dom->loadHTML(self::ensureString($convert)); + $convert = $this->securityScanner->scan($content); + $lowend = "\u{80}"; + $highend = "\u{10ffff}"; + $regexp = "/[$lowend-$highend]/u"; + /** @var callable */ + $callback = [self::class, 'replaceNonAscii']; + $convert = preg_replace_callback($regexp, $callback, $convert); + $loaded = ($convert === null) ? false : $dom->loadHTML($convert); } catch (Throwable $e) { $loaded = false; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php index 86b3ee38199..e3de4731749 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods.php @@ -2,16 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Reader; -use DateTime; use DOMAttr; use DOMDocument; use DOMElement; use DOMNode; -use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Reader\Ods\AutoFilter; use PhpOffice\PhpSpreadsheet\Reader\Ods\DefinedNames; +use PhpOffice\PhpSpreadsheet\Reader\Ods\FormulaTranslator; use PhpOffice\PhpSpreadsheet\Reader\Ods\PageSettings; use PhpOffice\PhpSpreadsheet\Reader\Ods\Properties as DocumentProperties; use PhpOffice\PhpSpreadsheet\Reader\Security\XmlScanner; @@ -21,6 +20,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use Throwable; use XMLReader; use ZipArchive; @@ -106,7 +106,7 @@ class Ods extends BaseReader $xml->read(); while ($xml->read()) { // Quickly jump through to the office:body node - while ($xml->name !== 'office:body') { + while (self::getXmlName($xml) !== 'office:body') { if ($xml->isEmptyElement) { $xml->read(); } else { @@ -115,12 +115,13 @@ class Ods extends BaseReader } // Now read each node until we find our first table:table node while ($xml->read()) { - if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + $xmlName = self::getXmlName($xml); + if ($xmlName == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { // Loop through each table:table node reading the table:name attribute for each worksheet name do { $worksheetNames[] = $xml->getAttribute('table:name'); $xml->next(); - } while ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); + } while (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT); } } } @@ -153,7 +154,7 @@ class Ods extends BaseReader $xml->read(); while ($xml->read()) { // Quickly jump through to the office:body node - while ($xml->name !== 'office:body') { + while (self::getXmlName($xml) !== 'office:body') { if ($xml->isEmptyElement) { $xml->read(); } else { @@ -162,7 +163,7 @@ class Ods extends BaseReader } // Now read each node until we find our first table:table node while ($xml->read()) { - if ($xml->name == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { + if (self::getXmlName($xml) == 'table:table' && $xml->nodeType == XMLReader::ELEMENT) { $worksheetNames[] = $xml->getAttribute('table:name'); $tmpInfo = [ @@ -177,7 +178,7 @@ class Ods extends BaseReader $currCells = 0; do { $xml->read(); - if ($xml->name == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { + if (self::getXmlName($xml) == 'table:table-row' && $xml->nodeType == XMLReader::ELEMENT) { $rowspan = $xml->getAttribute('table:number-rows-repeated'); $rowspan = empty($rowspan) ? 1 : $rowspan; $tmpInfo['totalRows'] += $rowspan; @@ -187,22 +188,22 @@ class Ods extends BaseReader $xml->read(); do { $doread = true; - if ($xml->name == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + if (self::getXmlName($xml) == 'table:table-cell' && $xml->nodeType == XMLReader::ELEMENT) { if (!$xml->isEmptyElement) { ++$currCells; $xml->next(); $doread = false; } - } elseif ($xml->name == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { + } elseif (self::getXmlName($xml) == 'table:covered-table-cell' && $xml->nodeType == XMLReader::ELEMENT) { $mergeSize = $xml->getAttribute('table:number-columns-repeated'); $currCells += (int) $mergeSize; } if ($doread) { $xml->read(); } - } while ($xml->name != 'table:table-row'); + } while (self::getXmlName($xml) != 'table:table-row'); } - } while ($xml->name != 'table:table'); + } while (self::getXmlName($xml) != 'table:table'); $tmpInfo['totalColumns'] = max($tmpInfo['totalColumns'], $currCells); $tmpInfo['lastColumnIndex'] = $tmpInfo['totalColumns'] - 1; @@ -216,14 +217,20 @@ class Ods extends BaseReader } /** - * Loads PhpSpreadsheet from file. + * Counteract Phpstan caching. * - * @return Spreadsheet + * @phpstan-impure */ - public function load(string $filename, int $flags = 0) + private static function getXmlName(XMLReader $xml): string { - $this->processFlags($flags); + return $xml->name; + } + /** + * Loads PhpSpreadsheet from file. + */ + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet + { // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -319,7 +326,7 @@ class Ods extends BaseReader } $spreadsheet->setActiveSheetIndex($worksheetID); - if ($worksheetName) { + if ($worksheetName || is_numeric($worksheetName)) { // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in // formula cells... during the load, all formulae should be correct, and we're simply // bringing the worksheet name in line with the formula, not the reverse @@ -482,21 +489,7 @@ class Ods extends BaseReader case 'date': $type = DataType::TYPE_NUMERIC; $value = $cellData->getAttributeNS($officeNs, 'date-value'); - - $dateObj = new DateTime($value); - [$year, $month, $day, $hour, $minute, $second] = explode( - ' ', - $dateObj->format('Y m d H i s') - ); - - $dataValue = Date::formattedPHPToExcel( - (int) $year, - (int) $month, - (int) $day, - (int) $hour, - (int) $minute, - (int) $second - ); + $dataValue = Date::convertIsoDate($value); if ($dataValue != floor($dataValue)) { $formatting = NumberFormat::FORMAT_DATE_XLSX15 @@ -531,7 +524,7 @@ class Ods extends BaseReader if ($hasCalculatedValue) { $type = DataType::TYPE_FORMULA; $cellDataFormula = substr($cellDataFormula, strpos($cellDataFormula, ':=') + 1); - $cellDataFormula = $this->convertToExcelFormulaValue($cellDataFormula); + $cellDataFormula = FormulaTranslator::convertToExcelFormulaValue($cellDataFormula); } if ($cellData->hasAttributeNS($tableNs, 'number-columns-repeated')) { @@ -587,31 +580,7 @@ class Ods extends BaseReader } // Merged cells - if ( - $cellData->hasAttributeNS($tableNs, 'number-columns-spanned') - || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned') - ) { - if (($type !== DataType::TYPE_NULL) || (!$this->readDataOnly)) { - $columnTo = $columnID; - - if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) { - $columnIndex = Coordinate::columnIndexFromString($columnID); - $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned'); - $columnIndex -= 2; - - $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1); - } - - $rowTo = $rowID; - - if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) { - $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1; - } - - $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); - } - } + $this->processMergedCells($cellData, $tableNs, $type, $columnID, $rowID, $spreadsheet); ++$columnID; } @@ -620,6 +589,7 @@ class Ods extends BaseReader break; } } + $pageSettings->setVisibilityForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); $pageSettings->setPrintSettingsForWorksheet($spreadsheet->getActiveSheet(), $worksheetStyleName); ++$worksheetID; } @@ -660,7 +630,7 @@ class Ods extends BaseReader foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) { if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') { try { - $spreadsheet->setActiveSheetIndexByName($t->nodeValue); + $spreadsheet->setActiveSheetIndexByName($t->nodeValue ?? ''); } catch (Throwable $e) { // do nothing } @@ -687,7 +657,7 @@ class Ods extends BaseReader $setRow = $configItem->nodeValue; } } - $this->setSelected($spreadsheet, $wsname, $setCol, $setRow); + $this->setSelected($spreadsheet, $wsname, "$setCol", "$setRow"); } break; @@ -724,12 +694,7 @@ class Ods extends BaseReader // Multiple spaces? /** @var DOMAttr $cAttr */ $cAttr = $child->attributes->getNamedItem('c'); - if ($cAttr) { - $multiplier = (int) $cAttr->nodeValue; - } else { - $multiplier = 1; - } - + $multiplier = self::getMultiplier($cAttr); $str .= str_repeat(' ', $multiplier); } @@ -741,6 +706,17 @@ class Ods extends BaseReader return $str; } + private static function getMultiplier(?DOMAttr $cAttr): int + { + if ($cAttr) { + $multiplier = (int) $cAttr->nodeValue; + } else { + $multiplier = 1; + } + + return $multiplier; + } + /** * @param string $is * @@ -754,31 +730,38 @@ class Ods extends BaseReader return $value; } - private function convertToExcelFormulaValue(string $openOfficeFormula): string - { - $temp = explode('"', $openOfficeFormula); - $tKey = false; - foreach ($temp as &$value) { - // Only replace in alternate array entries (i.e. non-quoted blocks) - if ($tKey = !$tKey) { - // Cell range reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); - // Cell reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); - // Cell range reference - $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); - // Simple cell reference - $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); - // Convert references to defined names/formulae - $value = str_replace('$$', '', $value ?? ''); + private function processMergedCells( + DOMElement $cellData, + string $tableNs, + string $type, + string $columnID, + int $rowID, + Spreadsheet $spreadsheet + ): void { + if ( + $cellData->hasAttributeNS($tableNs, 'number-columns-spanned') + || $cellData->hasAttributeNS($tableNs, 'number-rows-spanned') + ) { + if (($type !== DataType::TYPE_NULL) || ($this->readDataOnly === false)) { + $columnTo = $columnID; - $value = Calculation::translateSeparator(';', ',', $value, $inBraces); + if ($cellData->hasAttributeNS($tableNs, 'number-columns-spanned')) { + $columnIndex = Coordinate::columnIndexFromString($columnID); + $columnIndex += (int) $cellData->getAttributeNS($tableNs, 'number-columns-spanned'); + $columnIndex -= 2; + + $columnTo = Coordinate::stringFromColumnIndex($columnIndex + 1); + } + + $rowTo = $rowID; + + if ($cellData->hasAttributeNS($tableNs, 'number-rows-spanned')) { + $rowTo = $rowTo + (int) $cellData->getAttributeNS($tableNs, 'number-rows-spanned') - 1; + } + + $cellRange = $columnID . $rowID . ':' . $columnTo . $rowTo; + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } } - - // Then rebuild the formula string - $excelFormula = implode('"', $temp); - - return $excelFormula; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php index bdc8b3ffc37..1f5f975dec3 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/AutoFilter.php @@ -5,7 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Ods; use DOMElement; use DOMNode; -class AutoFilter extends BaseReader +class AutoFilter extends BaseLoader { public function read(DOMElement $workbookData): void { @@ -20,7 +20,7 @@ class AutoFilter extends BaseReader foreach ($autofilters->childNodes as $autofilter) { $autofilterRange = $this->getAttributeValue($autofilter, 'target-range-address'); if ($autofilterRange !== null) { - $baseAddress = $this->convertToExcelAddressValue($autofilterRange); + $baseAddress = FormulaTranslator::convertToExcelAddressValue($autofilterRange); $this->spreadsheet->getActiveSheet()->setAutoFilter($baseAddress); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseLoader.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseLoader.php new file mode 100644 index 00000000000..b06691f42ae --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseLoader.php @@ -0,0 +1,27 @@ +spreadsheet = $spreadsheet; + $this->tableNs = $tableNs; + } + + abstract public function read(DOMElement $workbookData): void; +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseReader.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseReader.php deleted file mode 100644 index 17e2d4d573b..00000000000 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/BaseReader.php +++ /dev/null @@ -1,77 +0,0 @@ -spreadsheet = $spreadsheet; - $this->tableNs = $tableNs; - } - - abstract public function read(DOMElement $workbookData): void; - - protected function convertToExcelAddressValue(string $openOfficeAddress): string - { - $excelAddress = $openOfficeAddress; - - // Cell range 3-d reference - // As we don't support 3-d ranges, we're just going to take a quick and dirty approach - // and assume that the second worksheet reference is the same as the first - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\$?([^\.]+)\.([^\.]+)/miu', '$1!$2:$4', $excelAddress); - // Cell range reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+):\.([^\.]+)/miu', '$1!$2:$3', $excelAddress ?? ''); - // Cell reference in another sheet - $excelAddress = preg_replace('/\$?([^\.]+)\.([^\.]+)/miu', '$1!$2', $excelAddress ?? ''); - // Cell range reference - $excelAddress = preg_replace('/\.([^\.]+):\.([^\.]+)/miu', '$1:$2', $excelAddress ?? ''); - // Simple cell reference - $excelAddress = preg_replace('/\.([^\.]+)/miu', '$1', $excelAddress ?? ''); - - return $excelAddress ?? ''; - } - - protected function convertToExcelFormulaValue(string $openOfficeFormula): string - { - $temp = explode('"', $openOfficeFormula); - $tKey = false; - foreach ($temp as &$value) { - // @var string $value - // Only replace in alternate array entries (i.e. non-quoted blocks) - if ($tKey = !$tKey) { - // Cell range reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+):\.([^\.]+)\]/miu', '$1!$2:$3', $value); - // Cell reference in another sheet - $value = preg_replace('/\[\$?([^\.]+)\.([^\.]+)\]/miu', '$1!$2', $value ?? ''); - // Cell range reference - $value = preg_replace('/\[\.([^\.]+):\.([^\.]+)\]/miu', '$1:$2', $value ?? ''); - // Simple cell reference - $value = preg_replace('/\[\.([^\.]+)\]/miu', '$1', $value ?? ''); - // Convert references to defined names/formulae - $value = str_replace('$$', '', $value ?? ''); - - $value = Calculation::translateSeparator(';', ',', $value, $inBraces); - } - } - - // Then rebuild the formula string - $excelFormula = implode('"', $temp); - - return $excelFormula; - } -} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php index 6810a3c7268..e0ab8900aa4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/DefinedNames.php @@ -6,7 +6,7 @@ use DOMElement; use PhpOffice\PhpSpreadsheet\DefinedName; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -class DefinedNames extends BaseReader +class DefinedNames extends BaseLoader { public function read(DOMElement $workbookData): void { @@ -25,8 +25,8 @@ class DefinedNames extends BaseReader $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); $range = $definedNameElement->getAttributeNS($this->tableNs, 'cell-range-address'); - $baseAddress = $this->convertToExcelAddressValue($baseAddress); - $range = $this->convertToExcelAddressValue($range); + $baseAddress = FormulaTranslator::convertToExcelAddressValue($baseAddress); + $range = FormulaTranslator::convertToExcelAddressValue($range); $this->addDefinedName($baseAddress, $definedName, $range); } @@ -43,9 +43,9 @@ class DefinedNames extends BaseReader $baseAddress = $definedNameElement->getAttributeNS($this->tableNs, 'base-cell-address'); $expression = $definedNameElement->getAttributeNS($this->tableNs, 'expression'); - $baseAddress = $this->convertToExcelAddressValue($baseAddress); + $baseAddress = FormulaTranslator::convertToExcelAddressValue($baseAddress); $expression = substr($expression, strpos($expression, ':=') + 1); - $expression = $this->convertToExcelFormulaValue($expression); + $expression = FormulaTranslator::convertToExcelFormulaValue($expression); $this->addDefinedName($baseAddress, $definedName, $expression); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php new file mode 100644 index 00000000000..4abdf11e460 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/FormulaTranslator.php @@ -0,0 +1,96 @@ +officeNs = $styleDom->lookupNamespaceUri('office'); $this->stylesNs = $styleDom->lookupNamespaceUri('style'); $this->stylesFo = $styleDom->lookupNamespaceUri('fo'); + $this->tableNs = $styleDom->lookupNamespaceUri('table'); } private function readPageSettingStyles(DOMDocument $styleDom): void @@ -98,12 +124,33 @@ class PageSettings foreach ($styleXReferences as $styleXreferenceSet) { $styleXRefName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'name'); $stylePageLayoutName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'master-page-name'); + $styleFamilyName = $styleXreferenceSet->getAttributeNS($this->stylesNs, 'family'); + if (!empty($styleFamilyName) && $styleFamilyName === 'table') { + $styleVisibility = 'true'; + foreach ($styleXreferenceSet->getElementsByTagNameNS($this->stylesNs, 'table-properties') as $tableProperties) { + $styleVisibility = $tableProperties->getAttributeNS($this->tableNs, 'display'); + } + $this->tableStylesCrossReference[$styleXRefName] = $styleVisibility; + } if (!empty($stylePageLayoutName)) { $this->masterStylesCrossReference[$styleXRefName] = $stylePageLayoutName; } } } + public function setVisibilityForWorksheet(Worksheet $worksheet, string $styleName): void + { + if (!array_key_exists($styleName, $this->tableStylesCrossReference)) { + return; + } + + $worksheet->setSheetState( + $this->tableStylesCrossReference[$styleName] === 'false' + ? Worksheet::SHEETSTATE_HIDDEN + : Worksheet::SHEETSTATE_VISIBLE + ); + } + public function setPrintSettingsForWorksheet(Worksheet $worksheet, string $styleName): void { if (!array_key_exists($styleName, $this->masterStylesCrossReference)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/Properties.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/Properties.php index fc78936735d..4389f4b2fce 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/Properties.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Ods/Properties.php @@ -26,10 +26,11 @@ class Properties $this->setCoreProperties($docProps, $officePropertiesDC); } - $officePropertyMeta = []; + $officePropertyMeta = null; if (isset($namespacesMeta['dc'])) { $officePropertyMeta = $officePropertyData->children($namespacesMeta['meta']); } + $officePropertyMeta = $officePropertyMeta ?? []; foreach ($officePropertyMeta as $propertyName => $propertyValue) { $this->setMetaProperties($namespacesMeta, $propertyValue, $propertyName, $docProps); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php index 8155b838fa6..40008d01d6a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Security/XmlScanner.php @@ -52,7 +52,7 @@ class XmlScanner public static function threadSafeLibxmlDisableEntityLoaderAvailability() { - if (PHP_MAJOR_VERSION == 7) { + if (PHP_MAJOR_VERSION === 7) { switch (PHP_MINOR_VERSION) { case 2: return PHP_RELEASE_VERSION >= 1; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php index de3c6ce5a09..9de4013ad97 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Slk.php @@ -191,13 +191,9 @@ class Slk extends BaseReader /** * Loads PhpSpreadsheet from file. - * - * @return Spreadsheet */ - public function load(string $filename, int $flags = 0) + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { - $this->processFlags($flags); - // Create new Spreadsheet $spreadsheet = new Spreadsheet(); @@ -454,7 +450,7 @@ class Slk extends BaseReader break; case 'M': - $formatArray['font']['size'] = substr($rowDatum, 1) / 20; + $formatArray['font']['size'] = ((float) substr($rowDatum, 1)) / 20; break; case 'L': diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php index 6b2c2fd6e77..e2fae121b5e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx.php @@ -20,6 +20,7 @@ use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViewOptions; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\SheetViews; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Theme; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx\WorkbookView; use PhpOffice\PhpSpreadsheet\ReferenceHelper; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\Settings; @@ -30,6 +31,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Font; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Style\Color; +use PhpOffice\PhpSpreadsheet\Style\Font as StyleFont; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing; @@ -149,12 +151,17 @@ class Xlsx extends BaseReader private const REL_TO_MAIN = [ Namespaces::PURL_OFFICE_DOCUMENT => Namespaces::PURL_MAIN, + Namespaces::THUMBNAIL => '', ]; private const REL_TO_DRAWING = [ Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_DRAWING, ]; + private const REL_TO_CHART = [ + Namespaces::PURL_RELATIONSHIPS => Namespaces::PURL_CHART, + ]; + /** * Reads names of the worksheets from a file, without parsing the whole file to a Spreadsheet object. * @@ -224,7 +231,10 @@ class Xlsx extends BaseReader $worksheets = []; foreach ($relsWorkbook->Relationship as $elex) { $ele = self::getAttributes($elex); - if ((string) $ele['Type'] === "$namespace/worksheet") { + if ( + ((string) $ele['Type'] === "$namespace/worksheet") || + ((string) $ele['Type'] === "$namespace/chartsheet") + ) { $worksheets[(string) $ele['Id']] = $ele['Target']; } } @@ -284,7 +294,7 @@ class Xlsx extends BaseReader return $worksheetInfo; } - private static function castToBoolean($c) + private static function castToBoolean(SimpleXMLElement $c): bool { $value = isset($c->v) ? (string) $c->v : null; if ($value == '0') { @@ -296,18 +306,25 @@ class Xlsx extends BaseReader return (bool) $c->v; } - private static function castToError($c) + private static function castToError(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } - private static function castToString($c) + private static function castToString(?SimpleXMLElement $c): ?string { - return isset($c->v) ? (string) $c->v : null; + return isset($c, $c->v) ? (string) $c->v : null; } - private function castToFormula($c, $r, &$cellDataType, &$value, &$calculatedValue, &$sharedFormulas, $castBaseType): void + /** + * @param mixed $value + * @param mixed $calculatedValue + */ + private function castToFormula(?SimpleXMLElement $c, string $r, string &$cellDataType, &$value, &$calculatedValue, array &$sharedFormulas, string $castBaseType): void { + if ($c === null) { + return; + } $attr = $c->f->attributes(); $cellDataType = 'f'; $value = "={$c->f}"; @@ -366,6 +383,9 @@ class Xlsx extends BaseReader if (strpos($fileName, '//') !== false) { $fileName = substr($fileName, strpos($fileName, '//') + 1); } + // Relative paths generated by dirname($filename) when $filename + // has no path (i.e.files in root of the zip archive) + $fileName = (string) preg_replace('/^\.\//', '', $fileName); $fileName = File::realpath($fileName); // Sadly, some 3rd party xlsx generators don't use consistent case for filenaming @@ -377,16 +397,15 @@ class Xlsx extends BaseReader $contents = $archive->getFromName(substr($fileName, 1), 0, ZipArchive::FL_NOCASE); } - return $contents; + return ($contents === false) ? '' : $contents; } /** * Loads Spreadsheet from file. */ - public function load(string $filename, int $flags = 0): Spreadsheet + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { File::assertFile($filename, self::INITIAL_FILE); - $this->processFlags($flags); // Initialisations $excel = new Spreadsheet(); @@ -401,17 +420,21 @@ class Xlsx extends BaseReader // Read the theme first, because we need the colour scheme when reading the styles [$workbookBasename, $xmlNamespaceBase] = $this->getWorkbookBaseName(); - $wbRels = $this->loadZip("xl/_rels/${workbookBasename}.rels", Namespaces::RELATIONSHIPS); + $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; + $chartNS = self::REL_TO_CHART[$xmlNamespaceBase] ?? Namespaces::CHART; + $wbRels = $this->loadZip("xl/_rels/{$workbookBasename}.rels", Namespaces::RELATIONSHIPS); $theme = null; $this->styleReader = new Styles(); foreach ($wbRels->Relationship as $relx) { $rel = self::getAttributes($relx); $relTarget = (string) $rel['Target']; + if (substr($relTarget, 0, 4) === '/xl/') { + $relTarget = substr($relTarget, 4); + } switch ($rel['Type']) { case "$xmlNamespaceBase/theme": $themeOrderArray = ['lt1', 'dk1', 'lt2', 'dk2']; $themeOrderAdditional = count($themeOrderArray); - $drawingNS = self::REL_TO_DRAWING[$xmlNamespaceBase] ?? Namespaces::DRAWINGML; $xmlTheme = $this->loadZip("xl/{$relTarget}", $drawingNS); $xmlThemeName = self::getAttributes($xmlTheme); @@ -464,7 +487,7 @@ class Xlsx extends BaseReader $propertyReader->readCustomProperties($this->getFromZipArchive($zip, $relTarget)); break; - //Ribbon + //Ribbon case Namespaces::EXTENSIBILITY: $customUI = $relTarget; if ($customUI) { @@ -509,7 +532,13 @@ class Xlsx extends BaseReader $worksheets[(string) $ele['Id']] = $ele['Target']; break; - // a vbaProject ? (: some macros) + case Namespaces::CHARTSHEET: + if ($this->includeCharts === true) { + $worksheets[(string) $ele['Id']] = $ele['Target']; + } + + break; + // a vbaProject ? (: some macros) case Namespaces::VBA: $macros = $ele['Target']; @@ -538,16 +567,16 @@ class Xlsx extends BaseReader if ($xpath === null) { $xmlStyles = self::testSimpleXml(null); } else { - // I think Nonamespace is okay because I'm using xpath. - $xmlStyles = $this->loadZipNonamespace("$dir/$xpath[Target]", $mainNS); + $xmlStyles = $this->loadZip("$dir/$xpath[Target]", $mainNS); } - $xmlStyles->registerXPathNamespace('smm', Namespaces::MAIN); - $fills = self::xpathNoFalse($xmlStyles, 'smm:fills/smm:fill'); - $fonts = self::xpathNoFalse($xmlStyles, 'smm:fonts/smm:font'); - $borders = self::xpathNoFalse($xmlStyles, 'smm:borders/smm:border'); - $xfTags = self::xpathNoFalse($xmlStyles, 'smm:cellXfs/smm:xf'); - $cellXfTags = self::xpathNoFalse($xmlStyles, 'smm:cellStyleXfs/smm:xf'); + $palette = self::extractPalette($xmlStyles); + $this->styleReader->setWorkbookPalette($palette); + $fills = self::extractStyles($xmlStyles, 'fills', 'fill'); + $fonts = self::extractStyles($xmlStyles, 'fonts', 'font'); + $borders = self::extractStyles($xmlStyles, 'borders', 'border'); + $xfTags = self::extractStyles($xmlStyles, 'cellXfs', 'xf'); + $cellXfTags = self::extractStyles($xmlStyles, 'cellStyleXfs', 'xf'); $styles = []; $cellStyles = []; @@ -558,6 +587,7 @@ class Xlsx extends BaseReader if (isset($numFmts) && ($numFmts !== null)) { $numFmts->registerXPathNamespace('sml', $mainNS); } + $this->styleReader->setNamespace($mainNS); if (!$this->readDataOnly/* && $xmlStyles*/) { foreach ($xfTags as $xfTag) { $xf = self::getAttributes($xfTag); @@ -642,6 +672,7 @@ class Xlsx extends BaseReader } } $this->styleReader->setStyleXml($xmlStyles); + $this->styleReader->setNamespace($mainNS); $this->styleReader->setStyleBaseData($theme, $styles, $cellStyles); $dxfs = $this->styleReader->dxfs($this->readDataOnly); $styles = $this->styleReader->styles(); @@ -684,6 +715,13 @@ class Xlsx extends BaseReader continue; } + $sheetReferenceId = (string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id'); + if (isset($worksheets[$sheetReferenceId]) === false) { + ++$countSkippedSheets; + $mapSheetId[$oldSheetId] = null; + + continue; + } // Map old sheet id in original workbook to new sheet id. // They will differ if loadSheetsOnly() is being used $mapSheetId[$oldSheetId] = $oldSheetId - $countSkippedSheets; @@ -695,7 +733,8 @@ class Xlsx extends BaseReader // and we're simply bringing the worksheet name in line with the formula, not the // reverse $docSheet->setTitle((string) $eleSheetAttr['name'], false, false); - $fileWorksheet = (string) $worksheets[(string) self::getArrayItem(self::getAttributes($eleSheet, $xmlNamespaceBase), 'id')]; + + $fileWorksheet = (string) $worksheets[$sheetReferenceId]; $xmlSheet = $this->loadZipNoNamespace("$dir/$fileWorksheet", $mainNS); $xmlSheetNS = $this->loadZip("$dir/$fileWorksheet", $mainNS); @@ -708,9 +747,12 @@ class Xlsx extends BaseReader $xmlSheetMain = $xmlSheetNS->children($mainNS); // Setting Conditional Styles adjusts selected cells, so we need to execute this // before reading the sheet view data to get the actual selected cells - if (!$this->readDataOnly && $xmlSheet->conditionalFormatting) { + if (!$this->readDataOnly && ($xmlSheet->conditionalFormatting)) { (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->load(); } + if (!$this->readDataOnly && $xmlSheet->extLst) { + (new ConditionalStyles($docSheet, $xmlSheet, $dxfs))->loadFromExt($this->styleReader); + } if (isset($xmlSheetMain->sheetViews, $xmlSheetMain->sheetViews->sheetView)) { $sheetViews = new SheetViews($xmlSheetMain->sheetViews->sheetView, $docSheet); $sheetViews->load(); @@ -767,7 +809,12 @@ class Xlsx extends BaseReader break; case 'b': if (!isset($c->f)) { - $value = self::castToBoolean($c); + if (isset($c->v)) { + $value = self::castToBoolean($c); + } else { + $value = null; + $cellDataType = DATATYPE::TYPE_NULL; + } } else { // Formula $this->castToFormula($c, $r, $cellDataType, $value, $calculatedValue, $sharedFormulas, 'castToBoolean'); @@ -821,10 +868,12 @@ class Xlsx extends BaseReader // Assign value if ($cellDataType != '') { // it is possible, that datatype is numeric but with an empty string, which result in an error - if ($cellDataType === DataType::TYPE_NUMERIC && $value === '') { - $cellDataType = DataType::TYPE_STRING; + if ($cellDataType === DataType::TYPE_NUMERIC && ($value === '' || $value === null)) { + $cellDataType = DataType::TYPE_NULL; + } + if ($cellDataType !== DataType::TYPE_NULL) { + $cell->setValueExplicit($value, $cellDataType); } - $cell->setValueExplicit($value, $cellDataType); } else { $cell->setValue($value); } @@ -865,7 +914,7 @@ class Xlsx extends BaseReader foreach ($xmlSheet->mergeCells->mergeCell as $mergeCell) { $mergeRef = (string) $mergeCell['ref']; if (strpos($mergeRef, ':') !== false) { - $docSheet->mergeCells((string) $mergeCell['ref']); + $docSheet->mergeCells((string) $mergeCell['ref'], Worksheet::MERGE_CELL_CONTENT_HIDE); } } } @@ -881,7 +930,7 @@ class Xlsx extends BaseReader } foreach ($xmlSheet->extLst->ext->children('x14', true)->dataValidations->dataValidation as $item) { - $node = $xmlSheet->dataValidations->addChild('dataValidation'); + $node = self::testSimpleXml($xmlSheet->dataValidations)->addChild('dataValidation'); foreach ($item->attributes() ?? [] as $attr) { $node->addAttribute($attr->getName(), $attr); } @@ -1102,7 +1151,7 @@ class Xlsx extends BaseReader } // Header/footer images - if ($xmlSheet && $xmlSheet->legacyDrawingHF && !$this->readDataOnly) { + if ($xmlSheet && $xmlSheet->legacyDrawingHF) { if ($zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { $relsWorksheet = $this->loadZipNoNamespace(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels', Namespaces::RELATIONSHIPS); $vmlRelationship = ''; @@ -1171,14 +1220,23 @@ class Xlsx extends BaseReader . '/_rels/' . basename($fileWorksheet) . '.rels'; + if (substr($drawingFilename, 0, 7) === 'xl//xl/') { + $drawingFilename = substr($drawingFilename, 4); + } if ($zip->locateName($drawingFilename)) { $relsWorksheet = $this->loadZipNoNamespace($drawingFilename, Namespaces::RELATIONSHIPS); $drawings = []; foreach ($relsWorksheet->Relationship as $ele) { if ((string) $ele['Type'] === "$xmlNamespaceBase/drawing") { - $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + $eleTarget = (string) $ele['Target']; + if (substr($eleTarget, 0, 4) === '/xl/') { + $drawings[(string) $ele['Id']] = substr($eleTarget, 1); + } else { + $drawings[(string) $ele['Id']] = self::dirAdd("$dir/$fileWorksheet", $ele['Target']); + } } } + if ($xmlSheet->drawing && !$this->readDataOnly) { $unparsedDrawings = []; $fileDrawing = null; @@ -1187,6 +1245,7 @@ class Xlsx extends BaseReader $fileDrawing = $drawings[$drawingRelId]; $drawingFilename = dirname($fileDrawing) . '/_rels/' . basename($fileDrawing) . '.rels'; $relsDrawing = $this->loadZipNoNamespace($drawingFilename, $xmlNamespaceBase); + $images = []; $hyperlinks = []; if ($relsDrawing && $relsDrawing->Relationship) { @@ -1199,7 +1258,13 @@ class Xlsx extends BaseReader $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']); } elseif ($eleType === "$xmlNamespaceBase/chart") { if ($this->includeCharts) { - $charts[self::dirAdd($fileDrawing, $ele['Target'])] = [ + $eleTarget = (string) $ele['Target']; + if (substr($eleTarget, 0, 4) === '/xl/') { + $index = substr($eleTarget, 1); + } else { + $index = self::dirAdd($fileDrawing, $eleTarget); + } + $charts[$index] = [ 'id' => (string) $ele['Id'], 'sheet' => $docSheet->getTitle(), ]; @@ -1207,6 +1272,7 @@ class Xlsx extends BaseReader } } } + $xmlDrawing = $this->loadZipNoNamespace($fileDrawing, ''); $xmlDrawingChildren = $xmlDrawing->children(Namespaces::SPREADSHEET_DRAWING); @@ -1250,8 +1316,8 @@ class Xlsx extends BaseReader $objDrawing->setOffsetX((int) Drawing::EMUToPixels($oneCellAnchor->from->colOff)); $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff)); $objDrawing->setResizeProportional(false); - $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem((int) self::getAttributes($oneCellAnchor->ext), 'cx'))); - $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem((int) self::getAttributes($oneCellAnchor->ext), 'cy'))); + $objDrawing->setWidth(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cx'))); + $objDrawing->setHeight(Drawing::EMUToPixels(self::getArrayItem(self::getAttributes($oneCellAnchor->ext), 'cy'))); if ($xfrm) { $objDrawing->setRotation((int) Drawing::angleToDegrees(self::getArrayItem(self::getAttributes($xfrm), 'rot'))); } @@ -1290,6 +1356,7 @@ class Xlsx extends BaseReader 'width' => $width, 'height' => $height, 'worksheetTitle' => $docSheet->getTitle(), + 'oneCellAnchor' => true, ]; } } @@ -1302,6 +1369,11 @@ class Xlsx extends BaseReader $outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw; $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick; $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); + /** @scrutinizer ignore-call */ + $editAs = $twoCellAnchor->attributes(); + if (isset($editAs, $editAs['editAs'])) { + $objDrawing->setEditAs($editAs['editAs']); + } $objDrawing->setName((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name')); $objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr')); $embedImageKey = (string) self::getArrayItem( @@ -1328,6 +1400,12 @@ class Xlsx extends BaseReader $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff)); $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff)); + + $objDrawing->setCoordinates2(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1)); + + $objDrawing->setOffsetX2(Drawing::EMUToPixels($twoCellAnchor->to->colOff)); + $objDrawing->setOffsetY2(Drawing::EMUToPixels($twoCellAnchor->to->rowOff)); + $objDrawing->setResizeProportional(false); if ($xfrm) { @@ -1374,6 +1452,27 @@ class Xlsx extends BaseReader } } } + if ($xmlDrawingChildren->absoluteAnchor) { + foreach ($xmlDrawingChildren->absoluteAnchor as $absoluteAnchor) { + if (($this->includeCharts) && ($absoluteAnchor->graphicFrame)) { + $graphic = $absoluteAnchor->graphicFrame->children(Namespaces::DRAWINGML)->graphic; + /** @var SimpleXMLElement $chartRef */ + $chartRef = $graphic->graphicData->children(Namespaces::CHART)->chart; + $thisChart = (string) self::getAttributes($chartRef, $xmlNamespaceBase); + $width = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cx')[0]); + $height = Drawing::EMUToPixels((int) self::getArrayItem(self::getAttributes($absoluteAnchor->ext), 'cy')[0]); + + $chartDetails[$docSheet->getTitle() . '!' . $thisChart] = [ + 'fromCoordinate' => 'A1', + 'fromOffsetX' => 0, + 'fromOffsetY' => 0, + 'width' => $width, + 'height' => $height, + 'worksheetTitle' => $docSheet->getTitle(), + ]; + } + } + } if (empty($relsDrawing) && $xmlDrawing->count() == 0) { // Save Drawing without rels and children as unparsed $unparsedDrawings[$drawingRelId] = $xmlDrawing->asXML(); @@ -1393,7 +1492,7 @@ class Xlsx extends BaseReader } // unparsed drawing AlternateContent - $xmlAltDrawing = $this->loadZip($fileDrawing, Namespaces::COMPATIBILITY); + $xmlAltDrawing = $this->loadZip((string) $fileDrawing, Namespaces::COMPATIBILITY); if ($xmlAltDrawing->AlternateContent) { foreach ($xmlAltDrawing->AlternateContent as $alternateContent) { @@ -1459,16 +1558,21 @@ class Xlsx extends BaseReader break; case '_xlnm.Print_Area': - $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE); + $rangeSets = preg_split("/('?(?:.*?)'?(?:![A-Z0-9]+:[A-Z0-9]+)),?/", $extractedRange, -1, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_DELIM_CAPTURE) ?: []; $newRangeSets = []; foreach ($rangeSets as $rangeSet) { - [$sheetName, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + [, $rangeSet] = Worksheet::extractSheetTitle($rangeSet, true); + if (empty($rangeSet)) { + continue; + } if (strpos($rangeSet, ':') === false) { $rangeSet = $rangeSet . ':' . $rangeSet; } $newRangeSets[] = str_replace('$', '', $rangeSet); } - $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + if (count($newRangeSets) > 0) { + $docSheet->getPageSetup()->setPrintArea(implode(',', $newRangeSets)); + } break; default: @@ -1509,7 +1613,7 @@ class Xlsx extends BaseReader if (strpos((string) $definedName, '!') !== false) { $range[0] = str_replace("''", "'", $range[0]); $range[0] = str_replace("'", '', $range[0]); - if ($worksheet = $excel->getSheetByName($range[0])) { + if ($worksheet = $excel->getSheetByName($range[0])) { // @phpstan-ignore-line $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $worksheet, $extractedRange, true, $scope)); } else { $excel->addDefinedName(DefinedName::createInstance((string) $definedName['name'], $scope, $extractedRange, true, $scope)); @@ -1530,7 +1634,7 @@ class Xlsx extends BaseReader // Need to split on a comma or a space if not in quotes, and extract the first part. $definedNameValueParts = preg_split("/[ ,](?=([^']*'[^']*')*[^']*$)/miuU", $definedRange); // Extract sheet name - [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); + [$extractedSheetName] = Worksheet::extractSheetTitle((string) $definedNameValueParts[0], true); // @phpstan-ignore-line $extractedSheetName = trim($extractedSheetName, "'"); // Locate sheet @@ -1546,62 +1650,7 @@ class Xlsx extends BaseReader } } - $workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView; - if ((!$this->readDataOnly || !empty($this->loadSheetsOnly)) && !empty($workbookView)) { - $workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView)); - // active sheet index - $activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index - - // keep active sheet index if sheet is still loaded, else first sheet is set as the active - if (isset($mapSheetId[$activeTab]) && $mapSheetId[$activeTab] !== null) { - $excel->setActiveSheetIndex($mapSheetId[$activeTab]); - } else { - if ($excel->getSheetCount() == 0) { - $excel->createSheet(); - } - $excel->setActiveSheetIndex(0); - } - - if (isset($workbookViewAttributes->showHorizontalScroll)) { - $showHorizontalScroll = (string) $workbookViewAttributes->showHorizontalScroll; - $excel->setShowHorizontalScroll($this->castXsdBooleanToBool($showHorizontalScroll)); - } - - if (isset($workbookViewAttributes->showVerticalScroll)) { - $showVerticalScroll = (string) $workbookViewAttributes->showVerticalScroll; - $excel->setShowVerticalScroll($this->castXsdBooleanToBool($showVerticalScroll)); - } - - if (isset($workbookViewAttributes->showSheetTabs)) { - $showSheetTabs = (string) $workbookViewAttributes->showSheetTabs; - $excel->setShowSheetTabs($this->castXsdBooleanToBool($showSheetTabs)); - } - - if (isset($workbookViewAttributes->minimized)) { - $minimized = (string) $workbookViewAttributes->minimized; - $excel->setMinimized($this->castXsdBooleanToBool($minimized)); - } - - if (isset($workbookViewAttributes->autoFilterDateGrouping)) { - $autoFilterDateGrouping = (string) $workbookViewAttributes->autoFilterDateGrouping; - $excel->setAutoFilterDateGrouping($this->castXsdBooleanToBool($autoFilterDateGrouping)); - } - - if (isset($workbookViewAttributes->firstSheet)) { - $firstSheet = (string) $workbookViewAttributes->firstSheet; - $excel->setFirstSheetIndex((int) $firstSheet); - } - - if (isset($workbookViewAttributes->visibility)) { - $visibility = (string) $workbookViewAttributes->visibility; - $excel->setVisibility($visibility); - } - - if (isset($workbookViewAttributes->tabRatio)) { - $tabRatio = (string) $workbookViewAttributes->tabRatio; - $excel->setTabRatio((int) $tabRatio); - } - } + (new WorkbookView($excel))->viewSettings($xmlWorkbook, $mainNS, $mapSheetId, $this->readDataOnly); break; } @@ -1627,17 +1676,26 @@ class Xlsx extends BaseReader if ($this->includeCharts) { $chartEntryRef = ltrim((string) $contentType['PartName'], '/'); $chartElements = $this->loadZip($chartEntryRef); - $objChart = Chart::readChart($chartElements, basename($chartEntryRef, '.xml')); - + $chartReader = new Chart($chartNS, $drawingNS); + $objChart = $chartReader->readChart($chartElements, basename($chartEntryRef, '.xml')); if (isset($charts[$chartEntryRef])) { $chartPositionRef = $charts[$chartEntryRef]['sheet'] . '!' . $charts[$chartEntryRef]['id']; if (isset($chartDetails[$chartPositionRef])) { - $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); + $excel->getSheetByName($charts[$chartEntryRef]['sheet'])->addChart($objChart); // @phpstan-ignore-line $objChart->setWorksheet($excel->getSheetByName($charts[$chartEntryRef]['sheet'])); - $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + // For oneCellAnchor or absoluteAnchor positioned charts, + // toCoordinate is not in the data. Does it need to be calculated? if (array_key_exists('toCoordinate', $chartDetails[$chartPositionRef])) { - // For oneCellAnchor positioned charts, toCoordinate is not in the data. Does it need to be calculated? + // twoCellAnchor + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); $objChart->setBottomRightPosition($chartDetails[$chartPositionRef]['toCoordinate'], $chartDetails[$chartPositionRef]['toOffsetX'], $chartDetails[$chartPositionRef]['toOffsetY']); + } else { + // oneCellAnchor or absoluteAnchor (e.g. Chart sheet) + $objChart->setTopLeftPosition($chartDetails[$chartPositionRef]['fromCoordinate'], $chartDetails[$chartPositionRef]['fromOffsetX'], $chartDetails[$chartPositionRef]['fromOffsetY']); + $objChart->setBottomRightPosition('', $chartDetails[$chartPositionRef]['width'], $chartDetails[$chartPositionRef]['height']); + if (array_key_exists('oneCellAnchor', $chartDetails[$chartPositionRef])) { + $objChart->setOneCellAnchor($chartDetails[$chartPositionRef]['oneCellAnchor']); + } } } } @@ -1645,7 +1703,7 @@ class Xlsx extends BaseReader break; - // unparsed + // unparsed case 'application/vnd.ms-excel.controlproperties+xml': $unparsedLoadedData['override_content_types'][(string) $contentType['PartName']] = (string) $contentType['ContentType']; @@ -1672,24 +1730,28 @@ class Xlsx extends BaseReader $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $is->t)); } else { if (is_object($is->r)) { - /** @var SimpleXMLElement $run */ foreach ($is->r as $run) { if (!isset($run->rPr)) { $value->createText(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); } else { $objText = $value->createTextRun(StringHelper::controlCharacterOOXML2PHP((string) $run->t)); + $objFont = $objText->getFont() ?? new StyleFont(); - $attr = $run->rPr->rFont->attributes(); - if (isset($attr['val'])) { - $objText->getFont()->setName((string) $attr['val']); + if (isset($run->rPr->rFont)) { + $attr = $run->rPr->rFont->attributes(); + if (isset($attr['val'])) { + $objFont->setName((string) $attr['val']); + } } - $attr = $run->rPr->sz->attributes(); - if (isset($attr['val'])) { - $objText->getFont()->setSize((float) $attr['val']); + if (isset($run->rPr->sz)) { + $attr = $run->rPr->sz->attributes(); + if (isset($attr['val'])) { + $objFont->setSize((float) $attr['val']); + } } if (isset($run->rPr->color)) { - $objText->getFont()->setColor(new Color($this->styleReader->readColor($run->rPr->color))); + $objFont->setColor(new Color($this->styleReader->readColor($run->rPr->color))); } if (isset($run->rPr->b)) { $attr = $run->rPr->b->attributes(); @@ -1697,7 +1759,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setBold(true); + $objFont->setBold(true); } } if (isset($run->rPr->i)) { @@ -1706,7 +1768,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setItalic(true); + $objFont->setItalic(true); } } if (isset($run->rPr->vertAlign)) { @@ -1714,19 +1776,19 @@ class Xlsx extends BaseReader if (isset($attr['val'])) { $vertAlign = strtolower((string) $attr['val']); if ($vertAlign == 'superscript') { - $objText->getFont()->setSuperscript(true); + $objFont->setSuperscript(true); } if ($vertAlign == 'subscript') { - $objText->getFont()->setSubscript(true); + $objFont->setSubscript(true); } } } if (isset($run->rPr->u)) { $attr = $run->rPr->u->attributes(); if (!isset($attr['val'])) { - $objText->getFont()->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); + $objFont->setUnderline(\PhpOffice\PhpSpreadsheet\Style\Font::UNDERLINE_SINGLE); } else { - $objText->getFont()->setUnderline((string) $attr['val']); + $objFont->setUnderline((string) $attr['val']); } } if (isset($run->rPr->strike)) { @@ -1735,7 +1797,7 @@ class Xlsx extends BaseReader (isset($attr['val']) && self::boolean((string) $attr['val'])) || (!isset($attr['val'])) ) { - $objText->getFont()->setStrikethrough(true); + $objFont->setStrikethrough(true); } } } @@ -1788,17 +1850,30 @@ class Xlsx extends BaseReader } } + /** + * @param null|array|bool|SimpleXMLElement $array + * @param int|string $key + * + * @return mixed + */ private static function getArrayItem($array, $key = 0) { - return $array[$key] ?? null; + return ($array === null || is_bool($array)) ? null : ($array[$key] ?? null); } - private static function dirAdd($base, $add) + /** + * @param null|SimpleXMLElement|string $base + * @param null|SimpleXMLElement|string $add + */ + private static function dirAdd($base, $add): string { - return preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); + $base = (string) $base; + $add = (string) $add; + + return (string) preg_replace('~[^/]+/\.\./~', '', dirname($base) . "/$add"); } - private static function toCSSArray($style) + private static function toCSSArray(string $style): array { $style = self::stripWhiteSpaceFromStyleString($style); @@ -1812,15 +1887,15 @@ class Xlsx extends BaseReader } if (strpos($item[1], 'pt') !== false) { $item[1] = str_replace('pt', '', $item[1]); - $item[1] = Font::fontSizeToPixels($item[1]); + $item[1] = (string) Font::fontSizeToPixels((int) $item[1]); } if (strpos($item[1], 'in') !== false) { $item[1] = str_replace('in', '', $item[1]); - $item[1] = Font::inchSizeToPixels($item[1]); + $item[1] = (string) Font::inchSizeToPixels((int) $item[1]); } if (strpos($item[1], 'cm') !== false) { $item[1] = str_replace('cm', '', $item[1]); - $item[1] = Font::centimeterSizeToPixels($item[1]); + $item[1] = (string) Font::centimeterSizeToPixels((int) $item[1]); } $style[$item[0]] = $item[1]; @@ -1829,12 +1904,15 @@ class Xlsx extends BaseReader return $style; } - public static function stripWhiteSpaceFromStyleString($string) + public static function stripWhiteSpaceFromStyleString(string $string): string { return trim(str_replace(["\r", "\n", ' '], '', $string), ';'); } - private static function boolean($value) + /** + * @param mixed $value + */ + private static function boolean($value): bool { if (is_object($value)) { $value = (string) $value; @@ -1902,7 +1980,7 @@ class Xlsx extends BaseReader return $returnValue; } - private function readFormControlProperties(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readFormControlProperties(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -1929,7 +2007,7 @@ class Xlsx extends BaseReader unset($unparsedCtrlProps); } - private function readPrinterSettings(Spreadsheet $excel, $dir, $fileWorksheet, $docSheet, array &$unparsedLoadedData): void + private function readPrinterSettings(Spreadsheet $excel, string $dir, string $fileWorksheet, Worksheet $docSheet, array &$unparsedLoadedData): void { $zip = $this->zip; if (!$zip->locateName(dirname("$dir/$fileWorksheet") . '/_rels/' . basename($fileWorksheet) . '.rels')) { @@ -1956,29 +2034,6 @@ class Xlsx extends BaseReader unset($unparsedPrinterSettings); } - /** - * Convert an 'xsd:boolean' XML value to a PHP boolean value. - * A valid 'xsd:boolean' XML value can be one of the following - * four values: 'true', 'false', '1', '0'. It is case sensitive. - * - * Note that just doing '(bool) $xsdBoolean' is not safe, - * since '(bool) "false"' returns true. - * - * @see https://www.w3.org/TR/xmlschema11-2/#boolean - * - * @param string $xsdBoolean An XML string value of type 'xsd:boolean' - * - * @return bool Boolean value - */ - private function castXsdBooleanToBool($xsdBoolean) - { - if ($xsdBoolean === 'false') { - return false; - } - - return (bool) $xsdBoolean; - } - private function getWorkbookBaseName(): array { $workbookBasename = ''; @@ -2040,7 +2095,7 @@ class Xlsx extends BaseReader if ($xmlSheet && $xmlSheet->autoFilter) { // In older files, autofilter structure is defined in the worksheet file (new AutoFilter($docSheet, $xmlSheet))->load(); - } elseif ($xmlSheet && $xmlSheet->tableParts && $xmlSheet->tableParts['count'] > 0) { + } elseif ($xmlSheet && $xmlSheet->tableParts && (int) $xmlSheet->tableParts['count'] > 0) { // But for Office365, MS decided to make it all just a bit more complicated $this->readAutoFilterTablesInTablesFile($xmlSheet, $dir, $fileWorksheet, $zip, $docSheet); } @@ -2077,4 +2132,33 @@ class Xlsx extends BaseReader } } } + + private static function extractStyles(?SimpleXMLElement $sxml, string $node1, string $node2): array + { + $array = []; + if ($sxml && $sxml->{$node1}->{$node2}) { + foreach ($sxml->{$node1}->{$node2} as $node) { + $array[] = $node; + } + } + + return $array; + } + + private static function extractPalette(?SimpleXMLElement $sxml): array + { + $array = []; + if ($sxml && $sxml->colors->indexedColors) { + foreach ($sxml->colors->indexedColors->rgbColor as $node) { + if ($node !== null) { + $attr = $node->attributes(); + if (isset($attr['rgb'])) { + $array[] = (string) $attr['rgb']; + } + } + } + } + + return (count($array) === 64) ? $array : []; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php index b88f9056749..39328adb864 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/AutoFilter.php @@ -9,8 +9,10 @@ use SimpleXMLElement; class AutoFilter { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml) @@ -22,13 +24,13 @@ class AutoFilter public function load(): void { // Remove all "$" in the auto filter range - $autoFilterRange = preg_replace('/\$/', '', $this->worksheetXml->autoFilter['ref']); + $autoFilterRange = (string) preg_replace('/\$/', '', $this->worksheetXml->autoFilter['ref'] ?? ''); if (strpos($autoFilterRange, ':') !== false) { $this->readAutoFilter($autoFilterRange, $this->worksheetXml); } } - private function readAutoFilter($autoFilterRange, $xmlSheet): void + private function readAutoFilter(string $autoFilterRange, SimpleXMLElement $xmlSheet): void { $autoFilter = $this->worksheet->getAutoFilter(); $autoFilter->setRange($autoFilterRange); @@ -39,15 +41,15 @@ class AutoFilter if ($filterColumn->filters) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_FILTER); $filters = $filterColumn->filters; - if ((isset($filters['blank'])) && ($filters['blank'] == 1)) { + if ((isset($filters['blank'])) && ((int) $filters['blank'] == 1)) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', '')->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Standard filters are always an OR join, so no join rule needs to be set // Entries can be either filter elements foreach ($filters->filter as $filterRule) { // Operator is undefined, but always treated as EQUAL - $column->createRule()->setRule(null, (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); + $column->createRule()->setRule('', (string) $filterRule['val'])->setRuleType(Rule::AUTOFILTER_RULETYPE_FILTER); } // Or Date Group elements @@ -69,7 +71,7 @@ class AutoFilter foreach ($filters->dateGroupItem as $dateGroupItem) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', [ 'year' => (string) $dateGroupItem['year'], 'month' => (string) $dateGroupItem['month'], @@ -83,9 +85,9 @@ class AutoFilter } } - private function readCustomAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readCustomAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->customFilters) { + if (isset($filterColumn, $filterColumn->customFilters)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_CUSTOMFILTER); $customFilters = $filterColumn->customFilters; // Custom filters can an AND or an OR join; @@ -102,15 +104,15 @@ class AutoFilter } } - private function readDynamicAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readDynamicAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->dynamicFilter) { + if (isset($filterColumn, $filterColumn->dynamicFilter)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_DYNAMICFILTER); // We should only ever have one dynamic filter foreach ($filterColumn->dynamicFilter as $filterRule) { // Operator is undefined, but always treated as EQUAL $column->createRule()->setRule( - null, + '', (string) $filterRule['val'], (string) $filterRule['type'] )->setRuleType(Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER); @@ -124,9 +126,9 @@ class AutoFilter } } - private function readTopTenAutoFilter(SimpleXMLElement $filterColumn, Column $column): void + private function readTopTenAutoFilter(?SimpleXMLElement $filterColumn, Column $column): void { - if ($filterColumn->top10) { + if (isset($filterColumn, $filterColumn->top10)) { $column->setFilterType(Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER); // We should only ever have one top10 filter foreach ($filterColumn->top10 as $filterRule) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php index 1679f01f9f1..2f146458209 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/BaseParserClass.php @@ -4,7 +4,10 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; class BaseParserClass { - protected static function boolean($value) + /** + * @param mixed $value + */ + protected static function boolean($value): bool { if (is_object($value)) { $value = (string) $value; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 667e3674bac..c22334cac9c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -2,20 +2,37 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; +use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; +use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; +use PhpOffice\PhpSpreadsheet\Chart\Properties as ChartProperties; use PhpOffice\PhpSpreadsheet\Chart\Title; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\RichText\RichText; -use PhpOffice\PhpSpreadsheet\Style\Color; use PhpOffice\PhpSpreadsheet\Style\Font; use SimpleXMLElement; class Chart { + /** @var string */ + private $cNamespace; + + /** @var string */ + private $aNamespace; + + public function __construct(string $cNamespace = Namespaces::CHART, string $aNamespace = Namespaces::DRAWINGML) + { + $this->cNamespace = $cNamespace; + $this->aNamespace = $aNamespace; + } + /** * @param string $name * @param string $format @@ -25,7 +42,7 @@ class Chart private static function getAttribute(SimpleXMLElement $component, $name, $format) { $attributes = $component->attributes(); - if (isset($attributes[$name])) { + if (@isset($attributes[$name])) { if ($format == 'string') { return (string) $attributes[$name]; } elseif ($format == 'integer') { @@ -42,59 +59,151 @@ class Chart return null; } - private static function readColor($color, $background = false) - { - if (isset($color['rgb'])) { - return (string) $color['rgb']; - } elseif (isset($color['indexed'])) { - return Color::indexedColor($color['indexed'] - 7, $background)->getARGB(); - } - } - /** * @param string $chartName * * @return \PhpOffice\PhpSpreadsheet\Chart\Chart */ - public static function readChart(SimpleXMLElement $chartElements, $chartName) + public function readChart(SimpleXMLElement $chartElements, $chartName) { - $namespacesChartMeta = $chartElements->getNamespaces(true); - $chartElementsC = $chartElements->children($namespacesChartMeta['c']); + $chartElementsC = $chartElements->children($this->cNamespace); $XaxisLabel = $YaxisLabel = $legend = $title = null; $dispBlanksAs = $plotVisOnly = null; $plotArea = null; + $rotX = $rotY = $rAngAx = $perspective = null; + $xAxis = new Axis(); + $yAxis = new Axis(); + $autoTitleDeleted = null; + $chartNoFill = false; + $gradientArray = []; + $gradientLin = null; + $roundedCorners = false; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { + case 'spPr': + $possibleNoFill = $chartElementsC->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $chartNoFill = true; + } + + break; + case 'roundedCorners': + /** @var bool */ + $roundedCorners = self::getAttribute($chartElementsC->roundedCorners, 'val', 'boolean'); + + break; case 'chart': foreach ($chartElement as $chartDetailsKey => $chartDetails) { - $chartDetailsC = $chartDetails->children($namespacesChartMeta['c']); + $chartDetails = Xlsx::testSimpleXml($chartDetails); switch ($chartDetailsKey) { + case 'autoTitleDeleted': + /** @var bool */ + $autoTitleDeleted = self::getAttribute($chartElementsC->chart->autoTitleDeleted, 'val', 'boolean'); + + break; + case 'view3D': + $rotX = self::getAttribute($chartDetails->rotX, 'val', 'integer'); + $rotY = self::getAttribute($chartDetails->rotY, 'val', 'integer'); + $rAngAx = self::getAttribute($chartDetails->rAngAx, 'val', 'integer'); + $perspective = self::getAttribute($chartDetails->perspective, 'val', 'integer'); + + break; case 'plotArea': - $plotAreaLayout = $XaxisLable = $YaxisLable = null; + $plotAreaLayout = $XaxisLabel = $YaxisLabel = null; $plotSeries = $plotAttributes = []; + $catAxRead = false; + $plotNoFill = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { + case 'spPr': + $possibleNoFill = $chartDetails->spPr->children($this->aNamespace); + if (isset($possibleNoFill->noFill)) { + $plotNoFill = true; + } + if (isset($possibleNoFill->gradFill->gsLst)) { + foreach ($possibleNoFill->gradFill->gsLst->gs as $gradient) { + $gradient = Xlsx::testSimpleXml($gradient); + /** @var float */ + $pos = self::getAttribute($gradient, 'pos', 'float'); + $gradientArray[] = [ + $pos / ChartProperties::PERCENTAGE_MULTIPLIER, + new ChartColor($this->readColor($gradient)), + ]; + } + } + if (isset($possibleNoFill->gradFill->lin)) { + $gradientLin = ChartProperties::XmlToAngle((string) self::getAttribute($possibleNoFill->gradFill->lin, 'ang', 'string')); + } + + break; case 'layout': - $plotAreaLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $plotAreaLayout = $this->chartLayoutDetails($chartDetail); break; - case 'catAx': + case Axis::AXIS_TYPE_CATEGORY: + case Axis::AXIS_TYPE_DATE: + $catAxRead = true; if (isset($chartDetail->title)) { - $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + $XaxisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); } + $xAxis->setAxisType($chartDetailKey); + $this->readEffects($chartDetail, $xAxis); + if (isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $xAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + } + if (isset($chartDetail->majorGridlines)) { + $majorGridlines = new GridLines(); + if (isset($chartDetail->majorGridlines->spPr)) { + $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); + } + $xAxis->setMajorGridlines($majorGridlines); + } + if (isset($chartDetail->minorGridlines)) { + $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); + if (isset($chartDetail->minorGridlines->spPr)) { + $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); + } + $xAxis->setMinorGridlines($minorGridlines); + } + $this->setAxisProperties($chartDetail, $xAxis); break; - case 'dateAx': - if (isset($chartDetail->title)) { - $XaxisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); - } - - break; - case 'valAx': - if (isset($chartDetail->title, $chartDetail->axPos)) { - $axisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); + case Axis::AXIS_TYPE_VALUE: + $whichAxis = null; + $axPos = null; + if (isset($chartDetail->axPos)) { $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string'); + } + if ($catAxRead) { + $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); + } elseif (!empty($axPos)) { + switch ($axPos) { + case 't': + case 'b': + $whichAxis = $xAxis; + $xAxis->setAxisType($chartDetailKey); + + break; + case 'r': + case 'l': + $whichAxis = $yAxis; + $yAxis->setAxisType($chartDetailKey); + + break; + } + } + if (isset($chartDetail->title)) { + $axisLabel = $this->chartTitle($chartDetail->title->children($this->cNamespace)); switch ($axPos) { case 't': @@ -109,75 +218,103 @@ class Chart break; } } + $this->readEffects($chartDetail, $whichAxis); + if ($whichAxis !== null && isset($chartDetail->spPr)) { + $sppr = $chartDetail->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $axisColorArray = $this->readColor($sppr->solidFill); + $whichAxis->setFillParameters($axisColorArray['value'], $axisColorArray['alpha'], $axisColorArray['type']); + } + } + if ($whichAxis !== null && isset($chartDetail->majorGridlines)) { + $majorGridlines = new GridLines(); + if (isset($chartDetail->majorGridlines->spPr)) { + $this->readEffects($chartDetail->majorGridlines, $majorGridlines); + $this->readLineStyle($chartDetail->majorGridlines, $majorGridlines); + } + $whichAxis->setMajorGridlines($majorGridlines); + } + if ($whichAxis !== null && isset($chartDetail->minorGridlines)) { + $minorGridlines = new GridLines(); + $minorGridlines->activateObject(); + if (isset($chartDetail->minorGridlines->spPr)) { + $this->readEffects($chartDetail->minorGridlines, $minorGridlines); + $this->readLineStyle($chartDetail->minorGridlines, $minorGridlines); + } + $whichAxis->setMinorGridlines($minorGridlines); + } + $this->setAxisProperties($chartDetail, $whichAxis); break; case 'barChart': case 'bar3DChart': $barDirection = self::getAttribute($chartDetail->barDir, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotDirection($barDirection); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotDirection("$barDirection"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'lineChart': case 'line3DChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($chartDetail); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'areaChart': case 'area3DChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($chartDetail); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'doughnutChart': case 'pieChart': case 'pie3DChart': - $explosion = isset($chartDetail->ser->explosion); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($explosion); + $explosion = self::getAttribute($chartDetail->ser->explosion, 'val', 'string'); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$explosion"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'scatterChart': + /** @var string */ $scatterStyle = self::getAttribute($chartDetail->scatterStyle, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle($scatterStyle); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'bubbleChart': $bubbleScale = self::getAttribute($chartDetail->bubbleScale, 'val', 'integer'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($bubbleScale); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$bubbleScale"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'radarChart': + /** @var string */ $radarStyle = self::getAttribute($chartDetail->radarStyle, 'val', 'string'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); $plotSer->setPlotStyle($radarStyle); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'surfaceChart': case 'surface3DChart': $wireFrame = self::getAttribute($chartDetail->wireframe, 'val', 'boolean'); - $plotSer = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotSer->setPlotStyle($wireFrame); + $plotSer = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotSer->setPlotStyle("$wireFrame"); $plotSeries[] = $plotSer; - $plotAttributes = self::readChartAttributes($chartDetail); + $plotAttributes = $this->readChartAttributes($chartDetail); break; case 'stockChart': - $plotSeries[] = self::chartDataSeries($chartDetail, $namespacesChartMeta, $chartDetailKey); - $plotAttributes = self::readChartAttributes($plotAreaLayout); + $plotSeries[] = $this->chartDataSeries($chartDetail, $chartDetailKey); + $plotAttributes = $this->readChartAttributes($chartDetail); break; } @@ -186,7 +323,13 @@ class Chart $plotAreaLayout = new Layout(); } $plotArea = new PlotArea($plotAreaLayout, $plotSeries); - self::setChartAttributes($plotAreaLayout, $plotAttributes); + $this->setChartAttributes($plotAreaLayout, $plotAttributes); + if ($plotNoFill) { + $plotArea->setNoFill(true); + } + if (!empty($gradientArray)) { + $plotArea->setGradientFillProperties($gradientArray, $gradientLin); + } break; case 'plotVisOnly': @@ -198,7 +341,7 @@ class Chart break; case 'title': - $title = self::chartTitle($chartDetails, $namespacesChartMeta); + $title = $this->chartTitle($chartDetails); break; case 'legend': @@ -206,6 +349,7 @@ class Chart $legendLayout = null; $legendOverlay = false; foreach ($chartDetails as $chartDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($chartDetailKey) { case 'legendPos': $legendPos = self::getAttribute($chartDetail, 'val', 'string'); @@ -216,42 +360,71 @@ class Chart break; case 'layout': - $legendLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $legendLayout = $this->chartLayoutDetails($chartDetail); break; } } - $legend = new Legend($legendPos, $legendLayout, $legendOverlay); + $legend = new Legend("$legendPos", $legendLayout, (bool) $legendOverlay); break; } } } } - $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, $dispBlanksAs, $XaxisLabel, $YaxisLabel); + $chart = new \PhpOffice\PhpSpreadsheet\Chart\Chart($chartName, $title, $legend, $plotArea, $plotVisOnly, (string) $dispBlanksAs, $XaxisLabel, $YaxisLabel, $xAxis, $yAxis); + if ($chartNoFill) { + $chart->setNoFill(true); + } + $chart->setRoundedCorners($roundedCorners); + if (is_bool($autoTitleDeleted)) { + $chart->setAutoTitleDeleted($autoTitleDeleted); + } + if (is_int($rotX)) { + $chart->setRotX($rotX); + } + if (is_int($rotY)) { + $chart->setRotY($rotY); + } + if (is_int($rAngAx)) { + $chart->setRAngAx($rAngAx); + } + if (is_int($perspective)) { + $chart->setPerspective($perspective); + } return $chart; } - private static function chartTitle(SimpleXMLElement $titleDetails, array $namespacesChartMeta) + private function chartTitle(SimpleXMLElement $titleDetails): Title { $caption = []; $titleLayout = null; foreach ($titleDetails as $titleDetailKey => $chartDetail) { + $chartDetail = Xlsx::testSimpleXml($chartDetail); switch ($titleDetailKey) { case 'tx': - $titleDetails = $chartDetail->rich->children($namespacesChartMeta['a']); - foreach ($titleDetails as $titleKey => $titleDetail) { - switch ($titleKey) { - case 'p': - $titleDetailPart = $titleDetail->children($namespacesChartMeta['a']); - $caption[] = self::parseRichText($titleDetailPart); + if (isset($chartDetail->rich)) { + $titleDetails = $chartDetail->rich->children($this->aNamespace); + foreach ($titleDetails as $titleKey => $titleDetail) { + $titleDetail = Xlsx::testSimpleXml($titleDetail); + switch ($titleKey) { + case 'p': + $titleDetailPart = $titleDetail->children($this->aNamespace); + $caption[] = $this->parseRichText($titleDetailPart); + } + } + } elseif (isset($chartDetail->strRef->strCache)) { + foreach ($chartDetail->strRef->strCache->pt as $pt) { + if (isset($pt->v)) { + $caption[] = (string) $pt->v; + } } } break; case 'layout': - $titleLayout = self::chartLayoutDetails($chartDetail, $namespacesChartMeta); + $titleLayout = $this->chartLayoutDetails($chartDetail); break; } @@ -260,30 +433,31 @@ class Chart return new Title($caption, $titleLayout); } - private static function chartLayoutDetails($chartDetail, $namespacesChartMeta) + private function chartLayoutDetails(SimpleXMLElement $chartDetail): ?Layout { if (!isset($chartDetail->manualLayout)) { return null; } - $details = $chartDetail->manualLayout->children($namespacesChartMeta['c']); + $details = $chartDetail->manualLayout->children($this->cNamespace); if ($details === null) { return null; } $layout = []; foreach ($details as $detailKey => $detail) { + $detail = Xlsx::testSimpleXml($detail); $layout[$detailKey] = self::getAttribute($detail, 'val', 'string'); } return new Layout($layout); } - private static function chartDataSeries($chartDetail, $namespacesChartMeta, $plotType) + private function chartDataSeries(SimpleXMLElement $chartDetail, string $plotType): DataSeries { $multiSeriesType = null; $smoothLine = false; - $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = []; + $seriesLabel = $seriesCategory = $seriesValues = $plotOrder = $seriesBubbles = []; - $seriesDetailSet = $chartDetail->children($namespacesChartMeta['c']); + $seriesDetailSet = $chartDetail->children($this->cNamespace); foreach ($seriesDetailSet as $seriesDetailKey => $seriesDetails) { switch ($seriesDetailKey) { case 'grouping': @@ -293,7 +467,18 @@ class Chart case 'ser': $marker = null; $seriesIndex = ''; + $fillColor = null; + $pointSize = null; + $noFill = false; + $bubble3D = false; + $dptColors = []; + $markerFillColor = null; + $markerBorderColor = null; + $lineStyle = null; + $labelLayout = null; + $trendLines = []; foreach ($seriesDetails as $seriesKey => $seriesDetail) { + $seriesDetail = Xlsx::testSimpleXml($seriesDetail); switch ($seriesKey) { case 'idx': $seriesIndex = self::getAttribute($seriesDetail, 'val', 'integer'); @@ -305,11 +490,89 @@ class Chart break; case 'tx': - $seriesLabel[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + $seriesLabel[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail); + + break; + case 'spPr': + $children = $seriesDetail->children($this->aNamespace); + if (isset($children->ln)) { + $ln = $children->ln; + if (is_countable($ln->noFill) && count($ln->noFill) === 1) { + $noFill = true; + } + $lineStyle = new GridLines(); + $this->readLineStyle($seriesDetails, $lineStyle); + } + if (isset($children->effectLst)) { + if ($lineStyle === null) { + $lineStyle = new GridLines(); + } + $this->readEffects($seriesDetails, $lineStyle); + } + if (isset($children->solidFill)) { + $fillColor = new ChartColor($this->readColor($children->solidFill)); + } + + break; + case 'dPt': + $dptIdx = (int) self::getAttribute($seriesDetail->idx, 'val', 'string'); + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $arrayColors = $this->readColor($children->solidFill); + $dptColors[$dptIdx] = new ChartColor($arrayColors); + } + } + + break; + case 'trendline': + $trendLine = new TrendLine(); + $this->readLineStyle($seriesDetail, $trendLine); + /** @var ?string */ + $trendLineType = self::getAttribute($seriesDetail->trendlineType, 'val', 'string'); + /** @var ?bool */ + $dispRSqr = self::getAttribute($seriesDetail->dispRSqr, 'val', 'boolean'); + /** @var ?bool */ + $dispEq = self::getAttribute($seriesDetail->dispEq, 'val', 'boolean'); + /** @var ?int */ + $order = self::getAttribute($seriesDetail->order, 'val', 'integer'); + /** @var ?int */ + $period = self::getAttribute($seriesDetail->period, 'val', 'integer'); + /** @var ?float */ + $forward = self::getAttribute($seriesDetail->forward, 'val', 'float'); + /** @var ?float */ + $backward = self::getAttribute($seriesDetail->backward, 'val', 'float'); + /** @var ?float */ + $intercept = self::getAttribute($seriesDetail->intercept, 'val', 'float'); + /** @var ?string */ + $name = (string) $seriesDetail->name; + $trendLine->setTrendLineProperties( + $trendLineType, + $order, + $period, + $dispRSqr, + $dispEq, + $backward, + $forward, + $intercept, + $name + ); + $trendLines[] = $trendLine; break; case 'marker': $marker = self::getAttribute($seriesDetail->symbol, 'val', 'string'); + $pointSize = self::getAttribute($seriesDetail->size, 'val', 'string'); + $pointSize = is_numeric($pointSize) ? ((int) $pointSize) : null; + if (isset($seriesDetail->spPr)) { + $children = $seriesDetail->spPr->children($this->aNamespace); + if (isset($children->solidFill)) { + $markerFillColor = $this->readColor($children->solidFill); + } + if (isset($children->ln->solidFill)) { + $markerBorderColor = $this->readColor($children->ln->solidFill); + } + } break; case 'smooth': @@ -317,37 +580,154 @@ class Chart break; case 'cat': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail); break; case 'val': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'xVal': - $seriesCategory[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesCategory[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); break; case 'yVal': - $seriesValues[$seriesIndex] = self::chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker); + $seriesValues[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); + + break; + case 'bubbleSize': + $seriesBubbles[$seriesIndex] = $this->chartDataSeriesValueSet($seriesDetail, "$marker", $fillColor, "$pointSize"); + + break; + case 'bubble3D': + $bubble3D = self::getAttribute($seriesDetail, 'val', 'boolean'); + + break; + case 'dLbls': + $labelLayout = new Layout($this->readChartAttributes($seriesDetails)); break; } } + if ($labelLayout) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setLabelLayout($labelLayout); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setLabelLayout($labelLayout); + } + } + if ($noFill) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setScatterLines(false); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setScatterLines(false); + } + } + if ($lineStyle !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->copyLineStyles($lineStyle); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->copyLineStyles($lineStyle); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->copyLineStyles($lineStyle); + } + } + if ($bubble3D) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setBubble3D($bubble3D); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setBubble3D($bubble3D); + } + } + if (!empty($dptColors)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setFillColor($dptColors); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setFillColor($dptColors); + } + } + if ($markerFillColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerFillColor()->setColorPropertiesArray($markerFillColor); + } + } + if ($markerBorderColor !== null) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->getMarkerBorderColor()->setColorPropertiesArray($markerBorderColor); + } + } + if ($smoothLine) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setSmoothLine(true); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setSmoothLine(true); + } + } + if (!empty($trendLines)) { + if (isset($seriesLabel[$seriesIndex])) { + $seriesLabel[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesCategory[$seriesIndex])) { + $seriesCategory[$seriesIndex]->setTrendLines($trendLines); + } + if (isset($seriesValues[$seriesIndex])) { + $seriesValues[$seriesIndex]->setTrendLines($trendLines); + } + } } } + /** @phpstan-ignore-next-line */ + $series = new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); + $series->setPlotBubbleSizes($seriesBubbles); - return new DataSeries($plotType, $multiSeriesType, $plotOrder, $seriesLabel, $seriesCategory, $seriesValues, $smoothLine); + return $series; } - private static function chartDataSeriesValueSet($seriesDetail, $namespacesChartMeta, $marker = null) + /** + * @return mixed + */ + private function chartDataSeriesValueSet(SimpleXMLElement $seriesDetail, ?string $marker = null, ?ChartColor $fillColor = null, ?string $pointSize = null) { if (isset($seriesDetail->strRef)) { $seriesSource = (string) $seriesDetail->strRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->strRef->strCache)) { - $seriesData = self::chartDataSeriesValues($seriesDetail->strRef->strCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValues($seriesDetail->strRef->strCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -356,9 +736,9 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->numRef->numCache)) { - $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); + $seriesData = $this->chartDataSeriesValues($seriesDetail->numRef->numCache->children($this->cNamespace)); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -367,10 +747,10 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlStrRef)) { $seriesSource = (string) $seriesDetail->multiLvlStrRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->multiLvlStrRef->multiLvlStrCache)) { - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlStrRef->multiLvlStrCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -379,10 +759,10 @@ class Chart return $seriesValues; } elseif (isset($seriesDetail->multiLvlNumRef)) { $seriesSource = (string) $seriesDetail->multiLvlNumRef->f; - $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, null, null, $marker); + $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_STRING, $seriesSource, null, 0, null, $marker, $fillColor, "$pointSize"); if (isset($seriesDetail->multiLvlNumRef->multiLvlNumCache)) { - $seriesData = self::chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($namespacesChartMeta['c']), 's'); + $seriesData = $this->chartDataSeriesValuesMultiLevel($seriesDetail->multiLvlNumRef->multiLvlNumCache->children($this->cNamespace), 's'); $seriesValues ->setFormatCode($seriesData['formatCode']) ->setDataValues($seriesData['dataValues']); @@ -391,16 +771,27 @@ class Chart return $seriesValues; } + if (isset($seriesDetail->v)) { + return new DataSeriesValues( + DataSeriesValues::DATASERIES_TYPE_STRING, + null, + null, + 1, + [(string) $seriesDetail->v] + ); + } + return null; } - private static function chartDataSeriesValues($seriesValueSet, $dataType = 'n') + private function chartDataSeriesValues(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array { $seriesVal = []; $formatCode = ''; $pointCount = 0; foreach ($seriesValueSet as $seriesValueIdx => $seriesValue) { + $seriesValue = Xlsx::testSimpleXml($seriesValue); switch ($seriesValueIdx) { case 'ptCount': $pointCount = self::getAttribute($seriesValue, 'val', 'integer'); @@ -414,7 +805,7 @@ class Chart $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); if ($dataType == 's') { $seriesVal[$pointVal] = (string) $seriesValue->v; - } elseif ($seriesValue->v === Functions::NA()) { + } elseif ((string) $seriesValue->v === ExcelError::NA()) { $seriesVal[$pointVal] = null; } else { $seriesVal[$pointVal] = (float) $seriesValue->v; @@ -431,7 +822,7 @@ class Chart ]; } - private static function chartDataSeriesValuesMultiLevel($seriesValueSet, $dataType = 'n') + private function chartDataSeriesValuesMultiLevel(SimpleXMLElement $seriesValueSet, string $dataType = 'n'): array { $seriesVal = []; $formatCode = ''; @@ -452,7 +843,7 @@ class Chart $pointVal = self::getAttribute($seriesValue, 'idx', 'integer'); if ($dataType == 's') { $seriesVal[$pointVal][] = (string) $seriesValue->v; - } elseif ($seriesValue->v === Functions::NA()) { + } elseif ((string) $seriesValue->v === ExcelError::NA()) { $seriesVal[$pointVal] = null; } else { $seriesVal[$pointVal][] = (float) $seriesValue->v; @@ -470,77 +861,233 @@ class Chart ]; } - private static function parseRichText(SimpleXMLElement $titleDetailPart) + private function parseRichText(SimpleXMLElement $titleDetailPart): RichText { $value = new RichText(); - $objText = null; - foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { - if (isset($titleDetailElement->t)) { - $objText = $value->createTextRun((string) $titleDetailElement->t); + $defaultFontSize = null; + $defaultBold = null; + $defaultItalic = null; + $defaultUnderscore = null; + $defaultStrikethrough = null; + $defaultBaseline = null; + $defaultFontName = null; + $defaultLatin = null; + $defaultEastAsian = null; + $defaultComplexScript = null; + $defaultFontColor = null; + if (isset($titleDetailPart->pPr->defRPr)) { + /** @var ?int */ + $defaultFontSize = self::getAttribute($titleDetailPart->pPr->defRPr, 'sz', 'integer'); + /** @var ?bool */ + $defaultBold = self::getAttribute($titleDetailPart->pPr->defRPr, 'b', 'boolean'); + /** @var ?bool */ + $defaultItalic = self::getAttribute($titleDetailPart->pPr->defRPr, 'i', 'boolean'); + /** @var ?string */ + $defaultUnderscore = self::getAttribute($titleDetailPart->pPr->defRPr, 'u', 'string'); + /** @var ?string */ + $defaultStrikethrough = self::getAttribute($titleDetailPart->pPr->defRPr, 'strike', 'string'); + /** @var ?int */ + $defaultBaseline = self::getAttribute($titleDetailPart->pPr->defRPr, 'baseline', 'integer'); + if (isset($titleDetailPart->defRPr->rFont['val'])) { + $defaultFontName = (string) $titleDetailPart->defRPr->rFont['val']; } + if (isset($titleDetailPart->pPr->defRPr->latin)) { + /** @var ?string */ + $defaultLatin = self::getAttribute($titleDetailPart->pPr->defRPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->ea)) { + /** @var ?string */ + $defaultEastAsian = self::getAttribute($titleDetailPart->pPr->defRPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->cs)) { + /** @var ?string */ + $defaultComplexScript = self::getAttribute($titleDetailPart->pPr->defRPr->cs, 'typeface', 'string'); + } + if (isset($titleDetailPart->pPr->defRPr->solidFill)) { + $defaultFontColor = $this->readColor($titleDetailPart->pPr->defRPr->solidFill); + } + } + foreach ($titleDetailPart as $titleDetailElementKey => $titleDetailElement) { + if ( + (string) $titleDetailElementKey !== 'r' + || !isset($titleDetailElement->t) + ) { + continue; + } + $objText = $value->createTextRun((string) $titleDetailElement->t); + if ($objText->getFont() === null) { + // @codeCoverageIgnoreStart + continue; + // @codeCoverageIgnoreEnd + } + $fontSize = null; + $bold = null; + $italic = null; + $underscore = null; + $strikethrough = null; + $baseline = null; + $fontName = null; + $latinName = null; + $eastAsian = null; + $complexScript = null; + $fontColor = null; + $underlineColor = null; if (isset($titleDetailElement->rPr)) { + // not used now, not sure it ever was, grandfathering if (isset($titleDetailElement->rPr->rFont['val'])) { - $objText->getFont()->setName((string) $titleDetailElement->rPr->rFont['val']); - } - - $fontSize = (self::getAttribute($titleDetailElement->rPr, 'sz', 'integer')); - if (is_int($fontSize)) { - $objText->getFont()->setSize(floor($fontSize / 100)); - } - - $fontColor = (self::getAttribute($titleDetailElement->rPr, 'color', 'string')); - if ($fontColor !== null) { - $objText->getFont()->setColor(new Color(self::readColor($fontColor))); + // @codeCoverageIgnoreStart + $fontName = (string) $titleDetailElement->rPr->rFont['val']; + // @codeCoverageIgnoreEnd + } + if (isset($titleDetailElement->rPr->latin)) { + /** @var ?string */ + $latinName = self::getAttribute($titleDetailElement->rPr->latin, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->ea)) { + /** @var ?string */ + $eastAsian = self::getAttribute($titleDetailElement->rPr->ea, 'typeface', 'string'); + } + if (isset($titleDetailElement->rPr->cs)) { + /** @var ?string */ + $complexScript = self::getAttribute($titleDetailElement->rPr->cs, 'typeface', 'string'); + } + /** @var ?int */ + $fontSize = self::getAttribute($titleDetailElement->rPr, 'sz', 'integer'); + + // not used now, not sure it ever was, grandfathering + if (isset($titleDetailElement->rPr->solidFill)) { + $fontColor = $this->readColor($titleDetailElement->rPr->solidFill); } + /** @var ?bool */ $bold = self::getAttribute($titleDetailElement->rPr, 'b', 'boolean'); - if ($bold !== null) { - $objText->getFont()->setBold($bold); - } + /** @var ?bool */ $italic = self::getAttribute($titleDetailElement->rPr, 'i', 'boolean'); - if ($italic !== null) { - $objText->getFont()->setItalic($italic); - } + /** @var ?int */ $baseline = self::getAttribute($titleDetailElement->rPr, 'baseline', 'integer'); - if ($baseline !== null) { - if ($baseline > 0) { - $objText->getFont()->setSuperscript(true); - } elseif ($baseline < 0) { - $objText->getFont()->setSubscript(true); - } + + /** @var ?string */ + $underscore = self::getAttribute($titleDetailElement->rPr, 'u', 'string'); + if (isset($titleDetailElement->rPr->uFill->solidFill)) { + $underlineColor = $this->readColor($titleDetailElement->rPr->uFill->solidFill); } - $underscore = (self::getAttribute($titleDetailElement->rPr, 'u', 'string')); - if ($underscore !== null) { - if ($underscore == 'sng') { - $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); - } elseif ($underscore == 'dbl') { - $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); - } else { - $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); - } - } + /** @var ?string */ + $strikethrough = self::getAttribute($titleDetailElement->rPr, 'strike', 'string'); + } - $strikethrough = (self::getAttribute($titleDetailElement->rPr, 's', 'string')); - if ($strikethrough !== null) { - if ($strikethrough == 'noStrike') { - $objText->getFont()->setStrikethrough(false); - } else { - $objText->getFont()->setStrikethrough(true); - } + $fontFound = false; + $latinName = $latinName ?? $defaultLatin; + if ($latinName !== null) { + $objText->getFont()->setLatin($latinName); + $fontFound = true; + } + $eastAsian = $eastAsian ?? $defaultEastAsian; + if ($eastAsian !== null) { + $objText->getFont()->setEastAsian($eastAsian); + $fontFound = true; + } + $complexScript = $complexScript ?? $defaultComplexScript; + if ($complexScript !== null) { + $objText->getFont()->setComplexScript($complexScript); + $fontFound = true; + } + $fontName = $fontName ?? $defaultFontName; + if ($fontName !== null) { + // @codeCoverageIgnoreStart + $objText->getFont()->setName($fontName); + $fontFound = true; + // @codeCoverageIgnoreEnd + } + + $fontSize = $fontSize ?? $defaultFontSize; + if (is_int($fontSize)) { + $objText->getFont()->setSize(floor($fontSize / 100)); + $fontFound = true; + } else { + $objText->getFont()->setSize(null, true); + } + + $fontColor = $fontColor ?? $defaultFontColor; + if (!empty($fontColor)) { + $objText->getFont()->setChartColor($fontColor); + $fontFound = true; + } + + $bold = $bold ?? $defaultBold; + if ($bold !== null) { + $objText->getFont()->setBold($bold); + $fontFound = true; + } + + $italic = $italic ?? $defaultItalic; + if ($italic !== null) { + $objText->getFont()->setItalic($italic); + $fontFound = true; + } + + $baseline = $baseline ?? $defaultBaseline; + if ($baseline !== null) { + $objText->getFont()->setBaseLine($baseline); + if ($baseline > 0) { + $objText->getFont()->setSuperscript(true); + } elseif ($baseline < 0) { + $objText->getFont()->setSubscript(true); } + $fontFound = true; + } + + $underscore = $underscore ?? $defaultUnderscore; + if ($underscore !== null) { + if ($underscore == 'sng') { + $objText->getFont()->setUnderline(Font::UNDERLINE_SINGLE); + } elseif ($underscore == 'dbl') { + $objText->getFont()->setUnderline(Font::UNDERLINE_DOUBLE); + } elseif ($underscore !== '') { + $objText->getFont()->setUnderline($underscore); + } else { + $objText->getFont()->setUnderline(Font::UNDERLINE_NONE); + } + $fontFound = true; + if ($underlineColor) { + $objText->getFont()->setUnderlineColor($underlineColor); + } + } + + $strikethrough = $strikethrough ?? $defaultStrikethrough; + if ($strikethrough !== null) { + $objText->getFont()->setStrikeType($strikethrough); + if ($strikethrough == 'noStrike') { + $objText->getFont()->setStrikethrough(false); + } else { + $objText->getFont()->setStrikethrough(true); + } + $fontFound = true; + } + if ($fontFound === false) { + $objText->setFont(null); } } return $value; } - private static function readChartAttributes($chartDetail) + /** + * @param ?SimpleXMLElement $chartDetail + */ + private function readChartAttributes($chartDetail): array { $plotAttributes = []; if (isset($chartDetail->dLbls)) { + if (isset($chartDetail->dLbls->dLblPos)) { + $plotAttributes['dLblPos'] = self::getAttribute($chartDetail->dLbls->dLblPos, 'val', 'string'); + } + if (isset($chartDetail->dLbls->numFmt)) { + $plotAttributes['numFmtCode'] = self::getAttribute($chartDetail->dLbls->numFmt, 'formatCode', 'string'); + $plotAttributes['numFmtLinked'] = self::getAttribute($chartDetail->dLbls->numFmt, 'sourceLinked', 'boolean'); + } if (isset($chartDetail->dLbls->showLegendKey)) { $plotAttributes['showLegendKey'] = self::getAttribute($chartDetail->dLbls->showLegendKey, 'val', 'string'); } @@ -562,6 +1109,21 @@ class Chart if (isset($chartDetail->dLbls->showLeaderLines)) { $plotAttributes['showLeaderLines'] = self::getAttribute($chartDetail->dLbls->showLeaderLines, 'val', 'string'); } + if (isset($chartDetail->dLbls->spPr)) { + $sppr = $chartDetail->dLbls->spPr->children($this->aNamespace); + if (isset($sppr->solidFill)) { + $plotAttributes['labelFillColor'] = new ChartColor($this->readColor($sppr->solidFill)); + } + if (isset($sppr->ln->solidFill)) { + $plotAttributes['labelBorderColor'] = new ChartColor($this->readColor($sppr->ln->solidFill)); + } + } + if (isset($chartDetail->dLbls->txPr)) { + $txpr = $chartDetail->dLbls->txPr->children($this->aNamespace); + if (isset($txpr->p->pPr->defRPr->solidFill)) { + $plotAttributes['labelFontColor'] = new ChartColor($this->readColor($txpr->p->pPr->defRPr->solidFill)); + } + } } return $plotAttributes; @@ -570,7 +1132,7 @@ class Chart /** * @param mixed $plotAttributes */ - private static function setChartAttributes(Layout $plotArea, $plotAttributes): void + private function setChartAttributes(Layout $plotArea, $plotAttributes): void { foreach ($plotAttributes as $plotAttributeKey => $plotAttributeValue) { switch ($plotAttributeKey) { @@ -605,4 +1167,250 @@ class Chart } } } + + private function readEffects(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void + { + if (!isset($chartObject, $chartDetail->spPr)) { + return; + } + $sppr = $chartDetail->spPr->children($this->aNamespace); + + if (isset($sppr->effectLst->glow)) { + $axisGlowSize = (float) self::getAttribute($sppr->effectLst->glow, 'rad', 'integer') / ChartProperties::POINTS_WIDTH_MULTIPLIER; + if ($axisGlowSize != 0.0) { + $colorArray = $this->readColor($sppr->effectLst->glow); + $chartObject->setGlowProperties($axisGlowSize, $colorArray['value'], $colorArray['alpha'], $colorArray['type']); + } + } + + if (isset($sppr->effectLst->softEdge)) { + /** @var string */ + $softEdgeSize = self::getAttribute($sppr->effectLst->softEdge, 'rad', 'string'); + if (is_numeric($softEdgeSize)) { + $chartObject->setSoftEdges((float) ChartProperties::xmlToPoints($softEdgeSize)); + } + } + + $type = ''; + foreach (self::SHADOW_TYPES as $shadowType) { + if (isset($sppr->effectLst->$shadowType)) { + $type = $shadowType; + + break; + } + } + if ($type !== '') { + /** @var string */ + $blur = self::getAttribute($sppr->effectLst->$type, 'blurRad', 'string'); + $blur = is_numeric($blur) ? ChartProperties::xmlToPoints($blur) : null; + /** @var string */ + $dist = self::getAttribute($sppr->effectLst->$type, 'dist', 'string'); + $dist = is_numeric($dist) ? ChartProperties::xmlToPoints($dist) : null; + /** @var string */ + $direction = self::getAttribute($sppr->effectLst->$type, 'dir', 'string'); + $direction = is_numeric($direction) ? ChartProperties::xmlToAngle($direction) : null; + $algn = self::getAttribute($sppr->effectLst->$type, 'algn', 'string'); + $rot = self::getAttribute($sppr->effectLst->$type, 'rotWithShape', 'string'); + $size = []; + foreach (['sx', 'sy'] as $sizeType) { + $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); + if (is_numeric($sizeValue)) { + $size[$sizeType] = ChartProperties::xmlToTenthOfPercent((string) $sizeValue); + } else { + $size[$sizeType] = null; + } + } + foreach (['kx', 'ky'] as $sizeType) { + $sizeValue = self::getAttribute($sppr->effectLst->$type, $sizeType, 'string'); + if (is_numeric($sizeValue)) { + $size[$sizeType] = ChartProperties::xmlToAngle((string) $sizeValue); + } else { + $size[$sizeType] = null; + } + } + $colorArray = $this->readColor($sppr->effectLst->$type); + $chartObject + ->setShadowProperty('effect', $type) + ->setShadowProperty('blur', $blur) + ->setShadowProperty('direction', $direction) + ->setShadowProperty('distance', $dist) + ->setShadowProperty('algn', $algn) + ->setShadowProperty('rotWithShape', $rot) + ->setShadowProperty('size', $size) + ->setShadowProperty('color', $colorArray); + } + } + + private const SHADOW_TYPES = [ + 'outerShdw', + 'innerShdw', + ]; + + private function readColor(SimpleXMLElement $colorXml): array + { + $result = [ + 'type' => null, + 'value' => null, + 'alpha' => null, + 'brightness' => null, + ]; + foreach (ChartColor::EXCEL_COLOR_TYPES as $type) { + if (isset($colorXml->$type)) { + $result['type'] = $type; + $result['value'] = self::getAttribute($colorXml->$type, 'val', 'string'); + if (isset($colorXml->$type->alpha)) { + /** @var string */ + $alpha = self::getAttribute($colorXml->$type->alpha, 'val', 'string'); + if (is_numeric($alpha)) { + $result['alpha'] = ChartColor::alphaFromXml($alpha); + } + } + if (isset($colorXml->$type->lumMod)) { + /** @var string */ + $brightness = self::getAttribute($colorXml->$type->lumMod, 'val', 'string'); + if (is_numeric($brightness)) { + $result['brightness'] = ChartColor::alphaFromXml($brightness); + } + } + + break; + } + } + + return $result; + } + + private function readLineStyle(SimpleXMLElement $chartDetail, ?ChartProperties $chartObject): void + { + if (!isset($chartObject, $chartDetail->spPr)) { + return; + } + $sppr = $chartDetail->spPr->children($this->aNamespace); + + if (!isset($sppr->ln)) { + return; + } + $lineWidth = null; + /** @var string */ + $lineWidthTemp = self::getAttribute($sppr->ln, 'w', 'string'); + if (is_numeric($lineWidthTemp)) { + $lineWidth = ChartProperties::xmlToPoints($lineWidthTemp); + } + /** @var string */ + $compoundType = self::getAttribute($sppr->ln, 'cmpd', 'string'); + /** @var string */ + $dashType = self::getAttribute($sppr->ln->prstDash, 'val', 'string'); + /** @var string */ + $capType = self::getAttribute($sppr->ln, 'cap', 'string'); + if (isset($sppr->ln->miter)) { + $joinType = ChartProperties::LINE_STYLE_JOIN_MITER; + } elseif (isset($sppr->ln->bevel)) { + $joinType = ChartProperties::LINE_STYLE_JOIN_BEVEL; + } else { + $joinType = ''; + } + $headArrowSize = ''; + $endArrowSize = ''; + /** @var string */ + $headArrowType = self::getAttribute($sppr->ln->headEnd, 'type', 'string'); + /** @var string */ + $headArrowWidth = self::getAttribute($sppr->ln->headEnd, 'w', 'string'); + /** @var string */ + $headArrowLength = self::getAttribute($sppr->ln->headEnd, 'len', 'string'); + /** @var string */ + $endArrowType = self::getAttribute($sppr->ln->tailEnd, 'type', 'string'); + /** @var string */ + $endArrowWidth = self::getAttribute($sppr->ln->tailEnd, 'w', 'string'); + /** @var string */ + $endArrowLength = self::getAttribute($sppr->ln->tailEnd, 'len', 'string'); + $chartObject->setLineStyleProperties( + $lineWidth, + $compoundType, + $dashType, + $capType, + $joinType, + $headArrowType, + $headArrowSize, + $endArrowType, + $endArrowSize, + $headArrowWidth, + $headArrowLength, + $endArrowWidth, + $endArrowLength + ); + $colorArray = $this->readColor($sppr->ln->solidFill); + $chartObject->getLineColor()->setColorPropertiesArray($colorArray); + } + + private function setAxisProperties(SimpleXMLElement $chartDetail, ?Axis $whichAxis): void + { + if (!isset($whichAxis)) { + return; + } + if (isset($chartDetail->delete)) { + $whichAxis->setAxisOption('hidden', (string) self::getAttribute($chartDetail->delete, 'val', 'string')); + } + if (isset($chartDetail->numFmt)) { + $whichAxis->setAxisNumberProperties( + (string) self::getAttribute($chartDetail->numFmt, 'formatCode', 'string'), + null, + (int) self::getAttribute($chartDetail->numFmt, 'sourceLinked', 'int') + ); + } + if (isset($chartDetail->crossBetween)) { + $whichAxis->setCrossBetween((string) self::getAttribute($chartDetail->crossBetween, 'val', 'string')); + } + if (isset($chartDetail->majorTickMark)) { + $whichAxis->setAxisOption('major_tick_mark', (string) self::getAttribute($chartDetail->majorTickMark, 'val', 'string')); + } + if (isset($chartDetail->minorTickMark)) { + $whichAxis->setAxisOption('minor_tick_mark', (string) self::getAttribute($chartDetail->minorTickMark, 'val', 'string')); + } + if (isset($chartDetail->tickLblPos)) { + $whichAxis->setAxisOption('axis_labels', (string) self::getAttribute($chartDetail->tickLblPos, 'val', 'string')); + } + if (isset($chartDetail->crosses)) { + $whichAxis->setAxisOption('horizontal_crosses', (string) self::getAttribute($chartDetail->crosses, 'val', 'string')); + } + if (isset($chartDetail->crossesAt)) { + $whichAxis->setAxisOption('horizontal_crosses_value', (string) self::getAttribute($chartDetail->crossesAt, 'val', 'string')); + } + if (isset($chartDetail->scaling->orientation)) { + $whichAxis->setAxisOption('orientation', (string) self::getAttribute($chartDetail->scaling->orientation, 'val', 'string')); + } + if (isset($chartDetail->scaling->max)) { + $whichAxis->setAxisOption('maximum', (string) self::getAttribute($chartDetail->scaling->max, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->scaling->min)) { + $whichAxis->setAxisOption('minimum', (string) self::getAttribute($chartDetail->scaling->min, 'val', 'string')); + } + if (isset($chartDetail->majorUnit)) { + $whichAxis->setAxisOption('major_unit', (string) self::getAttribute($chartDetail->majorUnit, 'val', 'string')); + } + if (isset($chartDetail->minorUnit)) { + $whichAxis->setAxisOption('minor_unit', (string) self::getAttribute($chartDetail->minorUnit, 'val', 'string')); + } + if (isset($chartDetail->baseTimeUnit)) { + $whichAxis->setAxisOption('baseTimeUnit', (string) self::getAttribute($chartDetail->baseTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->majorTimeUnit)) { + $whichAxis->setAxisOption('majorTimeUnit', (string) self::getAttribute($chartDetail->majorTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->minorTimeUnit)) { + $whichAxis->setAxisOption('minorTimeUnit', (string) self::getAttribute($chartDetail->minorTimeUnit, 'val', 'string')); + } + if (isset($chartDetail->txPr)) { + $children = $chartDetail->txPr->children($this->aNamespace); + if (isset($children->bodyPr)) { + /** @var string */ + $textRotation = self::getAttribute($children->bodyPr, 'rot', 'string'); + if (is_numeric($textRotation)) { + $whichAxis->setAxisOption('textRotation', (string) ChartProperties::xmlToAngle($textRotation)); + } + } + } + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php index 2a1e2afd8da..3470573385e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ColumnAndRowAttributes.php @@ -10,8 +10,10 @@ use SimpleXMLElement; class ColumnAndRowAttributes extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -120,7 +122,7 @@ class ColumnAndRowAttributes extends BaseParserClass } } - private function isFilteredColumn(IReadFilter $readFilter, $columnCoordinate, array $rowsAttributes) + private function isFilteredColumn(IReadFilter $readFilter, string $columnCoordinate, array $rowsAttributes): bool { foreach ($rowsAttributes as $rowCoordinate => $rowAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -131,7 +133,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readColumnAttributes(SimpleXMLElement $worksheetCols, $readDataOnly) + private function readColumnAttributes(SimpleXMLElement $worksheetCols, bool $readDataOnly): array { $columnAttributes = []; @@ -151,7 +153,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function readColumnRangeAttributes(SimpleXMLElement $column, $readDataOnly) + private function readColumnRangeAttributes(SimpleXMLElement $column, bool $readDataOnly): array { $columnAttributes = []; @@ -172,7 +174,7 @@ class ColumnAndRowAttributes extends BaseParserClass return $columnAttributes; } - private function isFilteredRow(IReadFilter $readFilter, $rowCoordinate, array $columnsAttributes) + private function isFilteredRow(IReadFilter $readFilter, int $rowCoordinate, array $columnsAttributes): bool { foreach ($columnsAttributes as $columnCoordinate => $columnAttributes) { if (!$readFilter->readCell($columnCoordinate, $rowCoordinate, $this->worksheet->getTitle())) { @@ -183,7 +185,7 @@ class ColumnAndRowAttributes extends BaseParserClass return false; } - private function readRowAttributes(SimpleXMLElement $worksheetRow, $readDataOnly) + private function readRowAttributes(SimpleXMLElement $worksheetRow, bool $readDataOnly): array { $rowAttributes = []; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php index dcd7ad12cb5..7d947bacf18 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/ConditionalStyles.php @@ -2,19 +2,30 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xlsx; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Styles as StyleReader; use PhpOffice\PhpSpreadsheet\Style\Conditional; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalDataBar; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormattingRuleExtension; use PhpOffice\PhpSpreadsheet\Style\ConditionalFormatting\ConditionalFormatValueObject; +use PhpOffice\PhpSpreadsheet\Style\Style as Style; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; +use stdClass; class ConditionalStyles { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; + /** + * @var array + */ + private $ns; + + /** @var array */ private $dxfs; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml, array $dxfs = []) @@ -33,7 +44,113 @@ class ConditionalStyles ); } - private function readConditionalStyles($xmlSheet) + public function loadFromExt(StyleReader $styleReader): void + { + $this->ns = $this->worksheetXml->getNamespaces(true); + $this->setConditionalsFromExt( + $this->readConditionalsFromExt($this->worksheetXml->extLst, $styleReader) + ); + } + + private function setConditionalsFromExt(array $conditionals): void + { + foreach ($conditionals as $conditionalRange => $cfRules) { + ksort($cfRules); + // Priority is used as the key for sorting; but may not start at 0, + // so we use array_values to reset the index after sorting. + $this->worksheet->getStyle($conditionalRange) + ->setConditionalStyles(array_values($cfRules)); + } + } + + private function readConditionalsFromExt(SimpleXMLElement $extLst, StyleReader $styleReader): array + { + $conditionals = []; + + if (isset($extLst->ext['uri']) && (string) $extLst->ext['uri'] === '{78C0D931-6437-407d-A8EE-F0AAD7539E65}') { + $conditionalFormattingRuleXml = $extLst->ext->children($this->ns['x14']); + if (!$conditionalFormattingRuleXml->conditionalFormattings) { + return []; + } + + foreach ($conditionalFormattingRuleXml->children($this->ns['x14']) as $extFormattingXml) { + $extFormattingRangeXml = $extFormattingXml->children($this->ns['xm']); + if (!$extFormattingRangeXml->sqref) { + continue; + } + + $sqref = (string) $extFormattingRangeXml->sqref; + $extCfRuleXml = $extFormattingXml->cfRule; + + $attributes = $extCfRuleXml->attributes(); + if (!$attributes) { + continue; + } + $conditionType = (string) $attributes->type; + if ( + !Conditional::isValidConditionType($conditionType) || + $conditionType === Conditional::CONDITION_DATABAR + ) { + continue; + } + + $priority = (int) $attributes->priority; + + $conditional = $this->readConditionalRuleFromExt($extCfRuleXml, $attributes); + $cfStyle = $this->readStyleFromExt($extCfRuleXml, $styleReader); + $conditional->setStyle($cfStyle); + $conditionals[$sqref][$priority] = $conditional; + } + } + + return $conditionals; + } + + private function readConditionalRuleFromExt(SimpleXMLElement $cfRuleXml, SimpleXMLElement $attributes): Conditional + { + $conditionType = (string) $attributes->type; + $operatorType = (string) $attributes->operator; + + $operands = []; + foreach ($cfRuleXml->children($this->ns['xm']) as $cfRuleOperandsXml) { + $operands[] = (string) $cfRuleOperandsXml; + } + + $conditional = new Conditional(); + $conditional->setConditionType($conditionType); + $conditional->setOperatorType($operatorType); + if ( + $conditionType === Conditional::CONDITION_CONTAINSTEXT || + $conditionType === Conditional::CONDITION_NOTCONTAINSTEXT || + $conditionType === Conditional::CONDITION_BEGINSWITH || + $conditionType === Conditional::CONDITION_ENDSWITH || + $conditionType === Conditional::CONDITION_TIMEPERIOD + ) { + $conditional->setText(array_pop($operands) ?? ''); + } + $conditional->setConditions($operands); + + return $conditional; + } + + private function readStyleFromExt(SimpleXMLElement $extCfRuleXml, StyleReader $styleReader): Style + { + $cfStyle = new Style(false, true); + if ($extCfRuleXml->dxf) { + $styleXML = $extCfRuleXml->dxf->children(); + + if ($styleXML->borders) { + $styleReader->readBorderStyle($cfStyle->getBorders(), $styleXML->borders); + } + if ($styleXML->fill) { + $styleReader->readFillStyle($cfStyle->getFill(), $styleXML->fill); + } + } + + return $cfStyle; + } + + private function readConditionalStyles(SimpleXMLElement $xmlSheet): array { $conditionals = []; foreach ($xmlSheet->conditionalFormatting as $conditional) { @@ -49,24 +166,25 @@ class ConditionalStyles return $conditionals; } - private function setConditionalStyles(Worksheet $worksheet, array $conditionals, $xmlExtLst): void + private function setConditionalStyles(Worksheet $worksheet, array $conditionals, SimpleXMLElement $xmlExtLst): void { - foreach ($conditionals as $ref => $cfRules) { + foreach ($conditionals as $cellRangeReference => $cfRules) { ksort($cfRules); $conditionalStyles = $this->readStyleRules($cfRules, $xmlExtLst); - // Extract all cell references in $ref - $cellBlocks = explode(' ', str_replace('$', '', strtoupper($ref))); + // Extract all cell references in $cellRangeReference + $cellBlocks = explode(' ', str_replace('$', '', strtoupper($cellRangeReference))); foreach ($cellBlocks as $cellBlock) { $worksheet->getStyle($cellBlock)->setConditionalStyles($conditionalStyles); } } } - private function readStyleRules($cfRules, $extLst) + private function readStyleRules(array $cfRules, SimpleXMLElement $extLst): array { $conditionalFormattingRuleExtensions = ConditionalFormattingRuleExtension::parseExtLstXml($extLst); $conditionalStyles = []; + foreach ($cfRules as $cfRule) { $objConditional = new Conditional(); $objConditional->setConditionType((string) $cfRule['type']); @@ -74,23 +192,32 @@ class ConditionalStyles if ((string) $cfRule['text'] != '') { $objConditional->setText((string) $cfRule['text']); + } elseif ((string) $cfRule['timePeriod'] != '') { + $objConditional->setText((string) $cfRule['timePeriod']); } if (isset($cfRule['stopIfTrue']) && (int) $cfRule['stopIfTrue'] === 1) { $objConditional->setStopIfTrue(true); } - if (count($cfRule->formula) > 1) { - foreach ($cfRule->formula as $formula) { - $objConditional->addCondition((string) $formula); + if (count($cfRule->formula) >= 1) { + foreach ($cfRule->formula as $formulax) { + $formula = (string) $formulax; + if ($formula === 'TRUE') { + $objConditional->addCondition(true); + } elseif ($formula === 'FALSE') { + $objConditional->addCondition(false); + } else { + $objConditional->addCondition($formula); + } } } else { - $objConditional->addCondition((string) $cfRule->formula); + $objConditional->addCondition(''); } if (isset($cfRule->dataBar)) { $objConditional->setDataBar( - $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) + $this->readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions) // @phpstan-ignore-line ); } else { $objConditional->setStyle(clone $this->dxfs[(int) ($cfRule['dxfId'])]); @@ -102,7 +229,10 @@ class ConditionalStyles return $conditionalStyles; } - private function readDataBarOfConditionalRule($cfRule, $conditionalFormattingRuleExtensions): ConditionalDataBar + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarOfConditionalRule($cfRule, array $conditionalFormattingRuleExtensions): ConditionalDataBar { $dataBar = new ConditionalDataBar(); //dataBar attribute @@ -134,7 +264,10 @@ class ConditionalStyles return $dataBar; } - private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, $conditionalFormattingRuleExtensions): void + /** + * @param SimpleXMLElement|stdClass $cfRule + */ + private function readDataBarExtLstOfConditionalRule(ConditionalDataBar $dataBar, $cfRule, array $conditionalFormattingRuleExtensions): void { if (isset($cfRule->extLst)) { $ns = $cfRule->extLst->getNamespaces(true); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php index b699cb57412..dac76230cc0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/DataValidations.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class DataValidations { + /** @var Worksheet */ private $worksheet; + /** @var SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, SimpleXMLElement $worksheetXml) @@ -22,7 +24,7 @@ class DataValidations { foreach ($this->worksheetXml->dataValidations->dataValidation as $dataValidation) { // Uppercase coordinate - $range = strtoupper($dataValidation['sqref']); + $range = strtoupper((string) $dataValidation['sqref']); $rangeSet = explode(' ', $range); foreach ($rangeSet as $range) { $stRange = $this->worksheet->shrinkRangeToFit($range); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php index 8488499629b..7d48c7967fd 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Hyperlinks.php @@ -9,8 +9,10 @@ use SimpleXMLElement; class Hyperlinks { + /** @var Worksheet */ private $worksheet; + /** @var array */ private $hyperlinks = []; public function __construct(Worksheet $workSheet) diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php index 54f56d7247b..57a88bb0b0a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Namespaces.php @@ -14,6 +14,8 @@ class Namespaces // This one used in Reader\Xlsx\Properties const CORE_PROPERTIES2 = 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties'; + const THUMBNAIL = 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail'; + const THEME = 'http://schemas.openxmlformats.org/package/2006/relationships/theme'; const COMPATIBILITY = 'http://schemas.openxmlformats.org/markup-compatibility/2006'; @@ -48,6 +50,8 @@ class Namespaces const WORKSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet'; + const CHARTSHEET = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet'; + const SCHEMA_MICROSOFT = 'http://schemas.microsoft.com/office/2006/relationships'; const EXTENSIBILITY = 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility'; @@ -72,5 +76,7 @@ class Namespaces const PURL_DRAWING = 'http://purl.oclc.org/ooxml/drawingml/main'; + const PURL_CHART = 'http://purl.oclc.org/ooxml/drawingml/chart'; + const PURL_WORKSHEET = 'http://purl.oclc.org/ooxml/officeDocument/relationships/worksheet'; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php index 56f18f98ba1..08decd6e405 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/PageSetup.php @@ -8,8 +8,10 @@ use SimpleXMLElement; class PageSetup extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -18,16 +20,17 @@ class PageSetup extends BaseParserClass $this->worksheetXml = $worksheetXml; } - public function load(array $unparsedLoadedData) + public function load(array $unparsedLoadedData): array { - if (!$this->worksheetXml) { + $worksheetXml = $this->worksheetXml; + if ($worksheetXml === null) { return $unparsedLoadedData; } - $this->margins($this->worksheetXml, $this->worksheet); - $unparsedLoadedData = $this->pageSetup($this->worksheetXml, $this->worksheet, $unparsedLoadedData); - $this->headerFooter($this->worksheetXml, $this->worksheet); - $this->pageBreaks($this->worksheetXml, $this->worksheet); + $this->margins($worksheetXml, $this->worksheet); + $unparsedLoadedData = $this->pageSetup($worksheetXml, $this->worksheet, $unparsedLoadedData); + $this->headerFooter($worksheetXml, $this->worksheet); + $this->pageBreaks($worksheetXml, $this->worksheet); return $unparsedLoadedData; } @@ -45,7 +48,7 @@ class PageSetup extends BaseParserClass } } - private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData) + private function pageSetup(SimpleXMLElement $xmlSheet, Worksheet $worksheet, array $unparsedLoadedData): array { if ($xmlSheet->pageSetup) { $docPageSetup = $worksheet->getPageSetup(); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php index 82b5172b58d..72addffd5ba 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Properties.php @@ -99,7 +99,7 @@ class Properties } /** - * @param array|false $array + * @param null|array|false $array * @param mixed $key */ private static function getArrayItem($array, $key = 0): ?SimpleXMLElement diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php index a302cc5692b..9c02da9f84e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/SheetViewOptions.php @@ -7,8 +7,10 @@ use SimpleXMLElement; class SheetViewOptions extends BaseParserClass { + /** @var Worksheet */ private $worksheet; + /** @var ?SimpleXMLElement */ private $worksheetXml; public function __construct(Worksheet $workSheet, ?SimpleXMLElement $worksheetXml = null) @@ -24,10 +26,11 @@ class SheetViewOptions extends BaseParserClass } if (isset($this->worksheetXml->sheetPr)) { - $this->tabColor($this->worksheetXml->sheetPr, $styleReader); - $this->codeName($this->worksheetXml->sheetPr); - $this->outlines($this->worksheetXml->sheetPr); - $this->pageSetup($this->worksheetXml->sheetPr); + $sheetPr = $this->worksheetXml->sheetPr; + $this->tabColor($sheetPr, $styleReader); + $this->codeName($sheetPr); + $this->outlines($sheetPr); + $this->pageSetup($sheetPr); } if (isset($this->worksheetXml->sheetFormatPr)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Styles.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Styles.php index 6f01c7457d1..f84aaa68a41 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Styles.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Styles.php @@ -24,6 +24,9 @@ class Styles extends BaseParserClass */ private $theme; + /** @var array */ + private $workbookPalette = []; + /** @var array */ private $styles = []; @@ -33,6 +36,42 @@ class Styles extends BaseParserClass /** @var SimpleXMLElement */ private $styleXml; + /** @var string */ + private $namespace = ''; + + public function setNamespace(string $namespace): void + { + $this->namespace = $namespace; + } + + public function setWorkbookPalette(array $palette): void + { + $this->workbookPalette = $palette; + } + + /** + * Cast SimpleXMLElement to bool to overcome Scrutinizer problem. + * + * @param mixed $value + */ + private static function castBool($value): bool + { + return (bool) $value; + } + + private function getStyleAttributes(SimpleXMLElement $value): SimpleXMLElement + { + $attr = null; + if (self::castBool($value)) { + $attr = $value->attributes(''); + if ($attr === null || count($attr) === 0) { + $attr = $value->attributes($this->namespace); + } + } + + return Xlsx::testSimpleXml($attr); + } + public function setStyleXml(SimpleXmlElement $styleXml): void { $this->styleXml = $styleXml; @@ -52,48 +91,62 @@ class Styles extends BaseParserClass public function readFontStyle(Font $fontStyle, SimpleXMLElement $fontStyleXml): void { - if (isset($fontStyleXml->name, $fontStyleXml->name['val'])) { - $fontStyle->setName((string) $fontStyleXml->name['val']); + if (isset($fontStyleXml->name)) { + $attr = $this->getStyleAttributes($fontStyleXml->name); + if (isset($attr['val'])) { + $fontStyle->setName((string) $attr['val']); + } } - if (isset($fontStyleXml->sz, $fontStyleXml->sz['val'])) { - $fontStyle->setSize((float) $fontStyleXml->sz['val']); + if (isset($fontStyleXml->sz)) { + $attr = $this->getStyleAttributes($fontStyleXml->sz); + if (isset($attr['val'])) { + $fontStyle->setSize((float) $attr['val']); + } } if (isset($fontStyleXml->b)) { - $fontStyle->setBold(!isset($fontStyleXml->b['val']) || self::boolean((string) $fontStyleXml->b['val'])); + $attr = $this->getStyleAttributes($fontStyleXml->b); + $fontStyle->setBold(!isset($attr['val']) || self::boolean((string) $attr['val'])); } if (isset($fontStyleXml->i)) { - $fontStyle->setItalic(!isset($fontStyleXml->i['val']) || self::boolean((string) $fontStyleXml->i['val'])); + $attr = $this->getStyleAttributes($fontStyleXml->i); + $fontStyle->setItalic(!isset($attr['val']) || self::boolean((string) $attr['val'])); } if (isset($fontStyleXml->strike)) { - $fontStyle->setStrikethrough( - !isset($fontStyleXml->strike['val']) || self::boolean((string) $fontStyleXml->strike['val']) - ); + $attr = $this->getStyleAttributes($fontStyleXml->strike); + $fontStyle->setStrikethrough(!isset($attr['val']) || self::boolean((string) $attr['val'])); } $fontStyle->getColor()->setARGB($this->readColor($fontStyleXml->color)); - if (isset($fontStyleXml->u) && !isset($fontStyleXml->u['val'])) { - $fontStyle->setUnderline(Font::UNDERLINE_SINGLE); - } elseif (isset($fontStyleXml->u, $fontStyleXml->u['val'])) { - $fontStyle->setUnderline((string) $fontStyleXml->u['val']); + if (isset($fontStyleXml->u)) { + $attr = $this->getStyleAttributes($fontStyleXml->u); + if (!isset($attr['val'])) { + $fontStyle->setUnderline(Font::UNDERLINE_SINGLE); + } else { + $fontStyle->setUnderline((string) $attr['val']); + } } - - if (isset($fontStyleXml->vertAlign, $fontStyleXml->vertAlign['val'])) { - $verticalAlign = strtolower((string) $fontStyleXml->vertAlign['val']); - if ($verticalAlign === 'superscript') { - $fontStyle->setSuperscript(true); - } elseif ($verticalAlign === 'subscript') { - $fontStyle->setSubscript(true); + if (isset($fontStyleXml->vertAlign)) { + $attr = $this->getStyleAttributes($fontStyleXml->vertAlign); + if (isset($attr['val'])) { + $verticalAlign = strtolower((string) $attr['val']); + if ($verticalAlign === 'superscript') { + $fontStyle->setSuperscript(true); + } elseif ($verticalAlign === 'subscript') { + $fontStyle->setSubscript(true); + } } } } private function readNumberFormat(NumberFormat $numfmtStyle, SimpleXMLElement $numfmtStyleXml): void { - if ($numfmtStyleXml->count() === 0) { + if ((string) $numfmtStyleXml['formatCode'] !== '') { + $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmtStyleXml['formatCode'])); + return; } - $numfmt = Xlsx::getAttributes($numfmtStyleXml); - if ($numfmt->count() > 0 && isset($numfmt['formatCode'])) { + $numfmt = $this->getStyleAttributes($numfmtStyleXml); + if (isset($numfmt['formatCode'])) { $numfmtStyle->setFormatCode(self::formatGeneral((string) $numfmt['formatCode'])); } } @@ -103,10 +156,11 @@ class Styles extends BaseParserClass if ($fillStyleXml->gradientFill) { /** @var SimpleXMLElement $gradientFill */ $gradientFill = $fillStyleXml->gradientFill[0]; - if (!empty($gradientFill['type'])) { - $fillStyle->setFillType((string) $gradientFill['type']); + $attr = $this->getStyleAttributes($gradientFill); + if (!empty($attr['type'])) { + $fillStyle->setFillType((string) $attr['type']); } - $fillStyle->setRotation((float) ($gradientFill['degree'])); + $fillStyle->setRotation((float) ($attr['degree'])); $gradientFill->registerXPathNamespace('sml', Namespaces::MAIN); $fillStyle->getStartColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=0]'))->color)); $fillStyle->getEndColor()->setARGB($this->readColor(self::getArrayItem($gradientFill->xpath('sml:stop[@position=1]'))->color)); @@ -121,9 +175,14 @@ class Styles extends BaseParserClass $defaultFillStyle = Fill::FILL_SOLID; } - $patternType = (string) $fillStyleXml->patternFill['patternType'] != '' - ? (string) $fillStyleXml->patternFill['patternType'] - : $defaultFillStyle; + $type = ''; + if ((string) $fillStyleXml->patternFill['patternType'] !== '') { + $type = (string) $fillStyleXml->patternFill['patternType']; + } else { + $attr = $this->getStyleAttributes($fillStyleXml->patternFill); + $type = (string) $attr['patternType']; + } + $patternType = ($type === '') ? $defaultFillStyle : $type; $fillStyle->setFillType($patternType); } @@ -131,14 +190,18 @@ class Styles extends BaseParserClass public function readBorderStyle(Borders $borderStyle, SimpleXMLElement $borderStyleXml): void { - $diagonalUp = self::boolean((string) $borderStyleXml['diagonalUp']); - $diagonalDown = self::boolean((string) $borderStyleXml['diagonalDown']); - if (!$diagonalUp && !$diagonalDown) { - $borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE); - } elseif ($diagonalUp && !$diagonalDown) { + $diagonalUp = $this->getAttribute($borderStyleXml, 'diagonalUp'); + $diagonalUp = self::boolean($diagonalUp); + $diagonalDown = $this->getAttribute($borderStyleXml, 'diagonalDown'); + $diagonalDown = self::boolean($diagonalDown); + if ($diagonalUp === false) { + if ($diagonalDown === false) { + $borderStyle->setDiagonalDirection(Borders::DIAGONAL_NONE); + } else { + $borderStyle->setDiagonalDirection(Borders::DIAGONAL_DOWN); + } + } elseif ($diagonalDown === false) { $borderStyle->setDiagonalDirection(Borders::DIAGONAL_UP); - } elseif (!$diagonalUp && $diagonalDown) { - $borderStyle->setDiagonalDirection(Borders::DIAGONAL_DOWN); } else { $borderStyle->setDiagonalDirection(Borders::DIAGONAL_BOTH); } @@ -150,10 +213,26 @@ class Styles extends BaseParserClass $this->readBorder($borderStyle->getDiagonal(), $borderStyleXml->diagonal); } + private function getAttribute(SimpleXMLElement $xml, string $attribute): string + { + $style = ''; + if ((string) $xml[$attribute] !== '') { + $style = (string) $xml[$attribute]; + } else { + $attr = $this->getStyleAttributes($xml); + if (isset($attr[$attribute])) { + $style = (string) $attr[$attribute]; + } + } + + return $style; + } + private function readBorder(Border $border, SimpleXMLElement $borderXml): void { - if (isset($borderXml['style'])) { - $border->setBorderStyle((string) $borderXml['style']); + $style = $this->getAttribute($borderXml, 'style'); + if ($style !== '') { + $border->setBorderStyle((string) $style); } if (isset($borderXml->color)) { $border->getColor()->setARGB($this->readColor($borderXml->color)); @@ -162,25 +241,25 @@ class Styles extends BaseParserClass public function readAlignmentStyle(Alignment $alignment, SimpleXMLElement $alignmentXml): void { - $alignment->setHorizontal((string) $alignmentXml['horizontal']); - $alignment->setVertical((string) $alignmentXml['vertical']); + $horizontal = $this->getAttribute($alignmentXml, 'horizontal'); + $alignment->setHorizontal($horizontal); + $vertical = $this->getAttribute($alignmentXml, 'vertical'); + $alignment->setVertical((string) $vertical); - $textRotation = 0; - if ((int) $alignmentXml['textRotation'] <= 90) { - $textRotation = (int) $alignmentXml['textRotation']; - } elseif ((int) $alignmentXml['textRotation'] > 90) { - $textRotation = 90 - (int) $alignmentXml['textRotation']; + $textRotation = (int) $this->getAttribute($alignmentXml, 'textRotation'); + if ($textRotation > 90) { + $textRotation = 90 - $textRotation; } + $alignment->setTextRotation($textRotation); - $alignment->setTextRotation((int) $textRotation); - $alignment->setWrapText(self::boolean((string) $alignmentXml['wrapText'])); - $alignment->setShrinkToFit(self::boolean((string) $alignmentXml['shrinkToFit'])); - $alignment->setIndent( - (int) ((string) $alignmentXml['indent']) > 0 ? (int) ((string) $alignmentXml['indent']) : 0 - ); - $alignment->setReadOrder( - (int) ((string) $alignmentXml['readingOrder']) > 0 ? (int) ((string) $alignmentXml['readingOrder']) : 0 - ); + $wrapText = $this->getAttribute($alignmentXml, 'wrapText'); + $alignment->setWrapText(self::boolean((string) $wrapText)); + $shrinkToFit = $this->getAttribute($alignmentXml, 'shrinkToFit'); + $alignment->setShrinkToFit(self::boolean((string) $shrinkToFit)); + $indent = (int) $this->getAttribute($alignmentXml, 'indent'); + $alignment->setIndent(max($indent, 0)); + $readingOrder = (int) $this->getAttribute($alignmentXml, 'readingOrder'); + $alignment->setReadOrder(max($readingOrder, 0)); } private static function formatGeneral(string $formatString): string @@ -223,8 +302,8 @@ class Styles extends BaseParserClass // protection if (isset($style->protection)) { - $this->readProtectionLocked($docStyle, $style); - $this->readProtectionHidden($docStyle, $style); + $this->readProtectionLocked($docStyle, $style->protection); + $this->readProtectionHidden($docStyle, $style->protection); } // top-level style settings @@ -235,13 +314,20 @@ class Styles extends BaseParserClass /** * Read protection locked attribute. - * - * @param SimpleXMLElement|stdClass $style */ - public function readProtectionLocked(Style $docStyle, $style): void + public function readProtectionLocked(Style $docStyle, SimpleXMLElement $style): void { - if (isset($style->protection['locked'])) { - if (self::boolean((string) $style->protection['locked'])) { + $locked = ''; + if ((string) $style['locked'] !== '') { + $locked = (string) $style['locked']; + } else { + $attr = $this->getStyleAttributes($style); + if (isset($attr['locked'])) { + $locked = (string) $attr['locked']; + } + } + if ($locked !== '') { + if (self::boolean($locked)) { $docStyle->getProtection()->setLocked(Protection::PROTECTION_PROTECTED); } else { $docStyle->getProtection()->setLocked(Protection::PROTECTION_UNPROTECTED); @@ -251,13 +337,20 @@ class Styles extends BaseParserClass /** * Read protection hidden attribute. - * - * @param SimpleXMLElement|stdClass $style */ - public function readProtectionHidden(Style $docStyle, $style): void + public function readProtectionHidden(Style $docStyle, SimpleXMLElement $style): void { - if (isset($style->protection['hidden'])) { - if (self::boolean((string) $style->protection['hidden'])) { + $hidden = ''; + if ((string) $style['hidden'] !== '') { + $hidden = (string) $style['hidden']; + } else { + $attr = $this->getStyleAttributes($style); + if (isset($attr['hidden'])) { + $hidden = (string) $attr['hidden']; + } + } + if ($hidden !== '') { + if (self::boolean((string) $hidden)) { $docStyle->getProtection()->setHidden(Protection::PROTECTION_PROTECTED); } else { $docStyle->getProtection()->setHidden(Protection::PROTECTION_UNPROTECTED); @@ -267,15 +360,22 @@ class Styles extends BaseParserClass public function readColor(SimpleXMLElement $color, bool $background = false): string { - if (isset($color['rgb'])) { - return (string) $color['rgb']; - } elseif (isset($color['indexed'])) { - return Color::indexedColor((int) ($color['indexed'] - 7), $background)->getARGB() ?? ''; - } elseif (isset($color['theme'])) { + $attr = $this->getStyleAttributes($color); + if (isset($attr['rgb'])) { + return (string) $attr['rgb']; + } + if (isset($attr['indexed'])) { + if (empty($this->workbookPalette)) { + return Color::indexedColor((int) ($attr['indexed'] - 7), $background)->getARGB() ?? ''; + } + + return Color::indexedColor((int) ($attr['indexed']), $background, $this->workbookPalette)->getARGB() ?? ''; + } + if (isset($attr['theme'])) { if ($this->theme !== null) { - $returnColour = $this->theme->getColourByIndex((int) $color['theme']); - if (isset($color['tint'])) { - $tintAdjust = (float) $color['tint']; + $returnColour = $this->theme->getColourByIndex((int) $attr['theme']); + if (isset($attr['tint'])) { + $tintAdjust = (float) $attr['tint']; $returnColour = Color::changeBrightness($returnColour ?? '', $tintAdjust); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Theme.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Theme.php index 1f2b863c782..706c4d1931d 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Theme.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/Theme.php @@ -41,9 +41,11 @@ class Theme } /** - * Get Theme Name. + * Not called by Reader, never accessible any other time. * * @return string + * + * @codeCoverageIgnore */ public function getThemeName() { @@ -51,9 +53,11 @@ class Theme } /** - * Get colour Scheme Name. + * Not called by Reader, never accessible any other time. * * @return string + * + * @codeCoverageIgnore */ public function getColourSchemeName() { @@ -69,25 +73,6 @@ class Theme */ public function getColourByIndex($index) { - if (isset($this->colourMap[$index])) { - return $this->colourMap[$index]; - } - - return null; - } - - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ - public function __clone() - { - $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if ((is_object($value)) && ($key != '_parent')) { - $this->$key = clone $value; - } else { - $this->$key = $value; - } - } + return $this->colourMap[$index] ?? null; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php new file mode 100644 index 00000000000..4743afbf9f8 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xlsx/WorkbookView.php @@ -0,0 +1,153 @@ +spreadsheet = $spreadsheet; + } + + /** + * @param mixed $mainNS + */ + public function viewSettings(SimpleXMLElement $xmlWorkbook, $mainNS, array $mapSheetId, bool $readDataOnly): void + { + if ($this->spreadsheet->getSheetCount() == 0) { + $this->spreadsheet->createSheet(); + } + // Default active sheet index to the first loaded worksheet from the file + $this->spreadsheet->setActiveSheetIndex(0); + + $workbookView = $xmlWorkbook->children($mainNS)->bookViews->workbookView; + if ($readDataOnly !== true && !empty($workbookView)) { + $workbookViewAttributes = self::testSimpleXml(self::getAttributes($workbookView)); + // active sheet index + $activeTab = (int) $workbookViewAttributes->activeTab; // refers to old sheet index + // keep active sheet index if sheet is still loaded, else first sheet is set as the active worksheet + if (isset($mapSheetId[$activeTab]) && $mapSheetId[$activeTab] !== null) { + $this->spreadsheet->setActiveSheetIndex($mapSheetId[$activeTab]); + } + + $this->horizontalScroll($workbookViewAttributes); + $this->verticalScroll($workbookViewAttributes); + $this->sheetTabs($workbookViewAttributes); + $this->minimized($workbookViewAttributes); + $this->autoFilterDateGrouping($workbookViewAttributes); + $this->firstSheet($workbookViewAttributes); + $this->visibility($workbookViewAttributes); + $this->tabRatio($workbookViewAttributes); + } + } + + /** + * @param mixed $value + */ + public static function testSimpleXml($value): SimpleXMLElement + { + return ($value instanceof SimpleXMLElement) + ? $value + : new SimpleXMLElement(''); + } + + public static function getAttributes(?SimpleXMLElement $value, string $ns = ''): SimpleXMLElement + { + return self::testSimpleXml($value === null ? $value : $value->attributes($ns)); + } + + /** + * Convert an 'xsd:boolean' XML value to a PHP boolean value. + * A valid 'xsd:boolean' XML value can be one of the following + * four values: 'true', 'false', '1', '0'. It is case sensitive. + * + * Note that just doing '(bool) $xsdBoolean' is not safe, + * since '(bool) "false"' returns true. + * + * @see https://www.w3.org/TR/xmlschema11-2/#boolean + * + * @param string $xsdBoolean An XML string value of type 'xsd:boolean' + * + * @return bool Boolean value + */ + private function castXsdBooleanToBool(string $xsdBoolean): bool + { + if ($xsdBoolean === 'false') { + return false; + } + + return (bool) $xsdBoolean; + } + + private function horizontalScroll(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->showHorizontalScroll)) { + $showHorizontalScroll = (string) $workbookViewAttributes->showHorizontalScroll; + $this->spreadsheet->setShowHorizontalScroll($this->castXsdBooleanToBool($showHorizontalScroll)); + } + } + + private function verticalScroll(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->showVerticalScroll)) { + $showVerticalScroll = (string) $workbookViewAttributes->showVerticalScroll; + $this->spreadsheet->setShowVerticalScroll($this->castXsdBooleanToBool($showVerticalScroll)); + } + } + + private function sheetTabs(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->showSheetTabs)) { + $showSheetTabs = (string) $workbookViewAttributes->showSheetTabs; + $this->spreadsheet->setShowSheetTabs($this->castXsdBooleanToBool($showSheetTabs)); + } + } + + private function minimized(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->minimized)) { + $minimized = (string) $workbookViewAttributes->minimized; + $this->spreadsheet->setMinimized($this->castXsdBooleanToBool($minimized)); + } + } + + private function autoFilterDateGrouping(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->autoFilterDateGrouping)) { + $autoFilterDateGrouping = (string) $workbookViewAttributes->autoFilterDateGrouping; + $this->spreadsheet->setAutoFilterDateGrouping($this->castXsdBooleanToBool($autoFilterDateGrouping)); + } + } + + private function firstSheet(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->firstSheet)) { + $firstSheet = (string) $workbookViewAttributes->firstSheet; + $this->spreadsheet->setFirstSheetIndex((int) $firstSheet); + } + } + + private function visibility(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->visibility)) { + $visibility = (string) $workbookViewAttributes->visibility; + $this->spreadsheet->setVisibility($visibility); + } + } + + private function tabRatio(SimpleXMLElement $workbookViewAttributes): void + { + if (isset($workbookViewAttributes->tabRatio)) { + $tabRatio = (string) $workbookViewAttributes->tabRatio; + $this->spreadsheet->setTabRatio((int) $tabRatio); + } + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php index 8552509e4d9..d8f0d9dcd04 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml.php @@ -18,6 +18,7 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use SimpleXMLElement; /** @@ -90,9 +91,9 @@ class Xml extends BaseReader // Retrieve charset encoding if (preg_match('//m', $data, $matches)) { $charSet = strtoupper($matches[1]); - if (1 == preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet)) { + if (preg_match('/^ISO-8859-\d[\dL]?$/i', $charSet) === 1) { $data = StringHelper::convertEncoding($data, 'UTF-8', $charSet); - $data = preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); + $data = (string) preg_replace('/()/um', '$1' . 'UTF-8' . '$2', $data, 1); } } $this->fileContents = $data; @@ -231,13 +232,9 @@ class Xml extends BaseReader /** * Loads Spreadsheet from file. - * - * @return Spreadsheet */ - public function load(string $filename, int $flags = 0) + protected function loadSpreadsheetFromFile(string $filename): Spreadsheet { - $this->processFlags($flags); - // Create new Spreadsheet $spreadsheet = new Spreadsheet(); $spreadsheet->removeSheetByIndex(0); @@ -368,7 +365,7 @@ class Xml extends BaseReader $rowTo = $rowTo + $cell_ss['MergeDown']; } $cellRange .= ':' . $columnTo . $rowTo; - $spreadsheet->getActiveSheet()->mergeCells($cellRange); + $spreadsheet->getActiveSheet()->mergeCells($cellRange, Worksheet::MERGE_CELL_CONTENT_HIDE); } $hasCalculatedValue = false; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Properties.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Properties.php index 1c3e421ae7e..9e10526e445 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Properties.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Reader/Xml/Properties.php @@ -44,7 +44,7 @@ class Properties foreach ($xml->CustomDocumentProperties[0] as $propertyName => $propertyValue) { $propertyAttributes = self::getAttributes($propertyValue, $namespaces['dt']); - $propertyName = preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); + $propertyName = (string) preg_replace_callback('/_x([0-9a-f]{4})_/i', [$this, 'hex2str'], $propertyName); $this->processCustomProperty($docProps, $propertyName, $propertyValue, $propertyAttributes); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php index 0d72b3055d6..3f53ed1d417 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/ReferenceHelper.php @@ -5,6 +5,9 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Worksheet\AutoFilter; +use PhpOffice\PhpSpreadsheet\Worksheet\Table; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class ReferenceHelper @@ -19,10 +22,15 @@ class ReferenceHelper /** * Instance of this class. * - * @var ReferenceHelper + * @var ?ReferenceHelper */ private static $instance; + /** + * @var CellReferenceHelper + */ + private $cellReferenceHelper; + /** * Get an instance of this class. * @@ -30,7 +38,7 @@ class ReferenceHelper */ public static function getInstance() { - if (!isset(self::$instance) || (self::$instance === null)) { + if (self::$instance === null) { self::$instance = new self(); } @@ -83,8 +91,8 @@ class ReferenceHelper */ public static function cellSort($a, $b) { - [$ac, $ar] = sscanf($a, '%[A-Z]%d'); - [$bc, $br] = sscanf($b, '%[A-Z]%d'); + sscanf($a, '%[A-Z]%d', $ac, $ar); + sscanf($b, '%[A-Z]%d', $bc, $br); if ($ar === $br) { return strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); @@ -104,8 +112,8 @@ class ReferenceHelper */ public static function cellReverseSort($a, $b) { - [$ac, $ar] = sscanf($a, '%[A-Z]%d'); - [$bc, $br] = sscanf($b, '%[A-Z]%d'); + sscanf($a, '%[A-Z]%d', $ac, $ar); + sscanf($b, '%[A-Z]%d', $bc, $br); if ($ar === $br) { return -strcasecmp(strlen($ac) . $ac, strlen($bc) . $bc); @@ -114,67 +122,32 @@ class ReferenceHelper return ($ar < $br) ? 1 : -1; } - /** - * Test whether a cell address falls within a defined range of cells. - * - * @param string $cellAddress Address of the cell we're testing - * @param int $beforeRow Number of the row we're inserting/deleting before - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before - * @param int $numberOfCols Number of columns to insert/delete (negative values indicate deletion) - * - * @return bool - */ - private static function cellAddressInDeleteRange($cellAddress, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfCols) - { - [$cellColumn, $cellRow] = Coordinate::coordinateFromString($cellAddress); - $cellColumnIndex = Coordinate::columnIndexFromString($cellColumn); - // Is cell within the range of rows/columns if we're deleting - if ( - $numberOfRows < 0 && - ($cellRow >= ($beforeRow + $numberOfRows)) && - ($cellRow < $beforeRow) - ) { - return true; - } elseif ( - $numberOfCols < 0 && - ($cellColumnIndex >= ($beforeColumnIndex + $numberOfCols)) && - ($cellColumnIndex < $beforeColumnIndex) - ) { - return true; - } - - return false; - } - /** * Update page breaks when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $beforeRow Number of the row we're inserting/deleting before * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustPageBreaks(Worksheet $worksheet, $beforeCellAddress, $beforeColumnIndex, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustPageBreaks(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aBreaks = $worksheet->getBreaks(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aBreaks, ['self', 'cellReverseSort']) : uksort($aBreaks, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aBreaks, [self::class, 'cellReverseSort']) + : uksort($aBreaks, [self::class, 'cellSort']); - foreach ($aBreaks as $key => $value) { - if (self::cellAddressInDeleteRange($key, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfColumns)) { + foreach ($aBreaks as $cellAddress => $value) { + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { // If we're deleting, then clear any defined breaks that are within the range // of rows/columns that we're deleting - $worksheet->setBreak($key, Worksheet::BREAK_NONE); + $worksheet->setBreak($cellAddress, Worksheet::BREAK_NONE); } else { // Otherwise update any affected breaks by inserting a new break at the appropriate point // and removing the old affected break - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->setBreak($newReference, $value) - ->setBreak($key, Worksheet::BREAK_NONE); + ->setBreak($cellAddress, Worksheet::BREAK_NONE); } } } @@ -184,22 +157,17 @@ class ReferenceHelper * Update cell comments when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $beforeColumnIndex Index number of the column we're inserting/deleting before - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $beforeRow Number of the row we're inserting/deleting before - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustComments($worksheet, $beforeCellAddress, $beforeColumnIndex, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustComments($worksheet): void { $aComments = $worksheet->getComments(); $aNewComments = []; // the new array of all comments - foreach ($aComments as $key => &$value) { + foreach ($aComments as $cellAddress => &$value) { // Any comments inside a deleted range will be ignored - if (!self::cellAddressInDeleteRange($key, $beforeRow, $numberOfRows, $beforeColumnIndex, $numberOfColumns)) { + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === false) { // Otherwise build a new array of comments indexed by the adjusted cell reference - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($cellAddress); $aNewComments[$newReference] = $value; } } @@ -211,44 +179,86 @@ class ReferenceHelper * Update hyperlinks when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustHyperlinks($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows): void { $aHyperlinkCollection = $worksheet->getHyperlinkCollection(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aHyperlinkCollection, ['self', 'cellReverseSort']) : uksort($aHyperlinkCollection, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aHyperlinkCollection, [self::class, 'cellReverseSort']) + : uksort($aHyperlinkCollection, [self::class, 'cellSort']); - foreach ($aHyperlinkCollection as $key => $value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + foreach ($aHyperlinkCollection as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($this->cellReferenceHelper->cellAddressInDeleteRange($cellAddress) === true) { + $worksheet->setHyperlink($cellAddress, null); + } elseif ($cellAddress !== $newReference) { $worksheet->setHyperlink($newReference, $value); - $worksheet->setHyperlink($key, null); + $worksheet->setHyperlink($cellAddress, null); } } } + /** + * Update conditional formatting styles when inserting/deleting rows/columns. + * + * @param Worksheet $worksheet The worksheet that we're editing + * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) + * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) + */ + protected function adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows): void + { + $aStyles = $worksheet->getConditionalStylesCollection(); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aStyles, [self::class, 'cellReverseSort']) + : uksort($aStyles, [self::class, 'cellSort']); + + foreach ($aStyles as $cellAddress => $cfRules) { + $worksheet->removeConditionalStyles($cellAddress); + $newReference = $this->updateCellReference($cellAddress); + + foreach ($cfRules as &$cfRule) { + /** @var Conditional $cfRule */ + $conditions = $cfRule->getConditions(); + foreach ($conditions as &$condition) { + if (is_string($condition)) { + $condition = $this->updateFormulaReferences( + $condition, + $this->cellReferenceHelper->beforeCellAddress(), + $numberOfColumns, + $numberOfRows, + $worksheet->getTitle(), + true + ); + } + } + $cfRule->setConditions($conditions); + } + $worksheet->setConditionalStyles($newReference, $cfRules); + } + } + /** * Update data validations when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $before Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustDataValidations(Worksheet $worksheet, $before, $numberOfColumns, $numberOfRows): void + protected function adjustDataValidations(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aDataValidationCollection = $worksheet->getDataValidationCollection(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aDataValidationCollection, ['self', 'cellReverseSort']) : uksort($aDataValidationCollection, ['self', 'cellSort']); + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aDataValidationCollection, [self::class, 'cellReverseSort']) + : uksort($aDataValidationCollection, [self::class, 'cellSort']); - foreach ($aDataValidationCollection as $key => $value) { - $newReference = $this->updateCellReference($key, $before, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { - $worksheet->setDataValidation($newReference, $value); - $worksheet->setDataValidation($key, null); + foreach ($aDataValidationCollection as $cellAddress => $dataValidation) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { + $dataValidation->setSqref($newReference); + $worksheet->setDataValidation($newReference, $dataValidation); + $worksheet->setDataValidation($cellAddress, null); } } } @@ -257,16 +267,13 @@ class ReferenceHelper * Update merged cells when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustMergeCells(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustMergeCells(Worksheet $worksheet): void { $aMergeCells = $worksheet->getMergeCells(); $aNewMergeCells = []; // the new array of all merge cells - foreach ($aMergeCells as $key => &$value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); + foreach ($aMergeCells as $cellAddress => &$value) { + $newReference = $this->updateCellReference($cellAddress); $aNewMergeCells[$newReference] = $newReference; } $worksheet->setMergeCells($aNewMergeCells); // replace the merge cells array @@ -276,20 +283,20 @@ class ReferenceHelper * Update protected cells when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustProtectedCells(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustProtectedCells(Worksheet $worksheet, $numberOfColumns, $numberOfRows): void { $aProtectedCells = $worksheet->getProtectedCells(); - ($numberOfColumns > 0 || $numberOfRows > 0) ? - uksort($aProtectedCells, ['self', 'cellReverseSort']) : uksort($aProtectedCells, ['self', 'cellSort']); - foreach ($aProtectedCells as $key => $value) { - $newReference = $this->updateCellReference($key, $beforeCellAddress, $numberOfColumns, $numberOfRows); - if ($key != $newReference) { + ($numberOfColumns > 0 || $numberOfRows > 0) + ? uksort($aProtectedCells, [self::class, 'cellReverseSort']) + : uksort($aProtectedCells, [self::class, 'cellSort']); + foreach ($aProtectedCells as $cellAddress => $value) { + $newReference = $this->updateCellReference($cellAddress); + if ($cellAddress !== $newReference) { $worksheet->protectCells($newReference, $value, true); - $worksheet->unprotectCells($key); + $worksheet->unprotectCells($cellAddress); } } } @@ -298,21 +305,19 @@ class ReferenceHelper * Update column dimensions when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) - * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustColumnDimensions(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows): void + protected function adjustColumnDimensions(Worksheet $worksheet): void { $aColumnDimensions = array_reverse($worksheet->getColumnDimensions(), true); if (!empty($aColumnDimensions)) { foreach ($aColumnDimensions as $objColumnDimension) { - $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1', $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($objColumnDimension->getColumnIndex() . '1'); [$newReference] = Coordinate::coordinateFromString($newReference); - if ($objColumnDimension->getColumnIndex() != $newReference) { + if ($objColumnDimension->getColumnIndex() !== $newReference) { $objColumnDimension->setColumnIndex($newReference); } } + $worksheet->refreshColumnDimensions(); } } @@ -321,22 +326,22 @@ class ReferenceHelper * Update row dimensions when inserting/deleting rows/columns. * * @param Worksheet $worksheet The worksheet that we're editing - * @param string $beforeCellAddress Insert/Delete before this cell address (e.g. 'A1') - * @param int $numberOfColumns Number of columns to insert/delete (negative values indicate deletion) * @param int $beforeRow Number of the row we're inserting/deleting before * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) */ - protected function adjustRowDimensions(Worksheet $worksheet, $beforeCellAddress, $numberOfColumns, $beforeRow, $numberOfRows): void + protected function adjustRowDimensions(Worksheet $worksheet, $beforeRow, $numberOfRows): void { $aRowDimensions = array_reverse($worksheet->getRowDimensions(), true); if (!empty($aRowDimensions)) { foreach ($aRowDimensions as $objRowDimension) { - $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex(), $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference('A' . $objRowDimension->getRowIndex()); [, $newReference] = Coordinate::coordinateFromString($newReference); - if ($objRowDimension->getRowIndex() != $newReference) { - $objRowDimension->setRowIndex($newReference); + $newRoweference = (int) $newReference; + if ($objRowDimension->getRowIndex() !== $newRoweference) { + $objRowDimension->setRowIndex($newRoweference); } } + $worksheet->refreshRowDimensions(); $copyDimension = $worksheet->getRowDimension($beforeRow - 1); @@ -358,10 +363,20 @@ class ReferenceHelper * @param int $numberOfRows Number of rows to insert/delete (negative values indicate deletion) * @param Worksheet $worksheet The worksheet that we're editing */ - public function insertNewBefore($beforeCellAddress, $numberOfColumns, $numberOfRows, Worksheet $worksheet): void - { + public function insertNewBefore( + string $beforeCellAddress, + int $numberOfColumns, + int $numberOfRows, + Worksheet $worksheet + ): void { $remove = ($numberOfColumns < 0 || $numberOfRows < 0); - $allCoordinates = $worksheet->getCoordinates(); + + if ( + $this->cellReferenceHelper === null || + $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } // Get coordinate of $beforeCellAddress [$beforeColumn, $beforeRow] = Coordinate::indexesFromString($beforeCellAddress); @@ -372,37 +387,39 @@ class ReferenceHelper // 1. Clear column strips if we are removing columns if ($numberOfColumns < 0 && $beforeColumn - 2 + $numberOfColumns > 0) { - for ($i = 1; $i <= $highestRow - 1; ++$i) { - for ($j = $beforeColumn - 1 + $numberOfColumns; $j <= $beforeColumn - 2; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } - } - } + $this->clearColumnStrips($highestRow, $beforeColumn, $numberOfColumns, $worksheet); } // 2. Clear row strips if we are removing rows if ($numberOfRows < 0 && $beforeRow - 1 + $numberOfRows > 0) { - for ($i = $beforeColumn - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$i) { - for ($j = $beforeRow + $numberOfRows; $j <= $beforeRow - 1; ++$j) { - $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; - $worksheet->removeConditionalStyles($coordinate); - if ($worksheet->cellExists($coordinate)) { - $worksheet->getCell($coordinate)->setValueExplicit('', DataType::TYPE_NULL); - $worksheet->getCell($coordinate)->setXfIndex(0); - } - } + $this->clearRowStrips($highestColumn, $beforeColumn, $beforeRow, $numberOfRows, $worksheet); + } + + // Find missing coordinates. This is important when inserting column before the last column + $cellCollection = $worksheet->getCellCollection(); + $missingCoordinates = array_filter( + array_map(function ($row) use ($highestColumn) { + return $highestColumn . $row; + }, range(1, $highestRow)), + function ($coordinate) use ($cellCollection) { + return $cellCollection->has($coordinate) === false; + } + ); + + // Create missing cells with null values + if (!empty($missingCoordinates)) { + foreach ($missingCoordinates as $coordinate) { + $worksheet->createNewCell($coordinate); } } - // Loop through cells, bottom-up, and change cell coordinate + $allCoordinates = $worksheet->getCoordinates(); if ($remove) { // It's faster to reverse and pop than to use unshift, especially with large cell collections $allCoordinates = array_reverse($allCoordinates); } + + // Loop through cells, bottom-up, and change cell coordinate while ($coordinate = array_pop($allCoordinates)) { $cell = $worksheet->getCell($coordinate); $cellIndex = Coordinate::columnIndexFromString($cell->getColumn()); @@ -420,7 +437,7 @@ class ReferenceHelper $worksheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); // Insert this cell at its new location - if ($cell->getDataType() == DataType::TYPE_FORMULA) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { // Formula should be adjusted $worksheet->getCell($newCoordinate) ->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle())); @@ -434,7 +451,7 @@ class ReferenceHelper } else { /* We don't need to update styles for rows/columns before our insertion position, but we do still need to adjust any formulae in those cells */ - if ($cell->getDataType() == DataType::TYPE_FORMULA) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { // Formula should be adjusted $cell->setValue($this->updateFormulaReferences($cell->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle())); } @@ -446,160 +463,82 @@ class ReferenceHelper $highestRow = $worksheet->getHighestRow(); if ($numberOfColumns > 0 && $beforeColumn - 2 > 0) { - for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { - // Style - $coordinate = Coordinate::stringFromColumnIndex($beforeColumn - 1) . $i; - if ($worksheet->cellExists($coordinate)) { - $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; - for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { - $worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($j) . $i, $cloned); - } - } - } - } + $this->duplicateStylesByColumn($worksheet, $beforeColumn, $beforeRow, $highestRow, $numberOfColumns); } if ($numberOfRows > 0 && $beforeRow - 1 > 0) { - for ($i = $beforeColumn; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) { - // Style - $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); - if ($worksheet->cellExists($coordinate)) { - $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); - $conditionalStyles = $worksheet->conditionalStylesExists($coordinate) ? - $worksheet->getConditionalStyles($coordinate) : false; - for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { - $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); - if ($conditionalStyles) { - $cloned = []; - foreach ($conditionalStyles as $conditionalStyle) { - $cloned[] = clone $conditionalStyle; - } - $worksheet->setConditionalStyles(Coordinate::stringFromColumnIndex($i) . $j, $cloned); - } - } - } - } + $this->duplicateStylesByRow($worksheet, $beforeColumn, $beforeRow, $highestColumn, $numberOfRows); } // Update worksheet: column dimensions - $this->adjustColumnDimensions($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustColumnDimensions($worksheet); // Update worksheet: row dimensions - $this->adjustRowDimensions($worksheet, $beforeCellAddress, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustRowDimensions($worksheet, $beforeRow, $numberOfRows); // Update worksheet: page breaks - $this->adjustPageBreaks($worksheet, $beforeCellAddress, $beforeColumn, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustPageBreaks($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: comments - $this->adjustComments($worksheet, $beforeCellAddress, $beforeColumn, $numberOfColumns, $beforeRow, $numberOfRows); + $this->adjustComments($worksheet); // Update worksheet: hyperlinks - $this->adjustHyperlinks($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustHyperlinks($worksheet, $numberOfColumns, $numberOfRows); + + // Update worksheet: conditional formatting styles + $this->adjustConditionalFormatting($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: data validations - $this->adjustDataValidations($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustDataValidations($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: merge cells - $this->adjustMergeCells($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustMergeCells($worksheet); // Update worksheet: protected cells - $this->adjustProtectedCells($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $this->adjustProtectedCells($worksheet, $numberOfColumns, $numberOfRows); // Update worksheet: autofilter - $autoFilter = $worksheet->getAutoFilter(); - $autoFilterRange = $autoFilter->getRange(); - if (!empty($autoFilterRange)) { - if ($numberOfColumns != 0) { - $autoFilterColumns = $autoFilter->getColumns(); - if (count($autoFilterColumns) > 0) { - $column = ''; - $row = 0; - sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); - $columnIndex = Coordinate::columnIndexFromString($column); - [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); - if ($columnIndex <= $rangeEnd[0]) { - if ($numberOfColumns < 0) { - // If we're actually deleting any columns that fall within the autofilter range, - // then we delete any rules for those columns - $deleteColumn = $columnIndex + $numberOfColumns - 1; - $deleteCount = abs($numberOfColumns); - for ($i = 1; $i <= $deleteCount; ++$i) { - if (isset($autoFilterColumns[Coordinate::stringFromColumnIndex($deleteColumn + 1)])) { - $autoFilter->clearColumn(Coordinate::stringFromColumnIndex($deleteColumn + 1)); - } - ++$deleteColumn; - } - } - $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + $this->adjustAutoFilter($worksheet, $beforeCellAddress, $numberOfColumns); - // Shuffle columns in autofilter range - if ($numberOfColumns > 0) { - $startColRef = $startCol; - $endColRef = $rangeEnd[0]; - $toColRef = $rangeEnd[0] + $numberOfColumns; - - do { - $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); - --$endColRef; - --$toColRef; - } while ($startColRef <= $endColRef); - } else { - // For delete, we shuffle from beginning to end to avoid overwriting - $startColID = Coordinate::stringFromColumnIndex($startCol); - $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); - $endColID = Coordinate::stringFromColumnIndex($rangeEnd[0] + 1); - do { - $autoFilter->shiftColumn($startColID, $toColID); - ++$startColID; - ++$toColID; - } while ($startColID != $endColID); - } - } - } - } - $worksheet->setAutoFilter($this->updateCellReference($autoFilterRange, $beforeCellAddress, $numberOfColumns, $numberOfRows)); - } + // Update worksheet: table + $this->adjustTable($worksheet, $beforeCellAddress, $numberOfColumns); // Update worksheet: freeze pane if ($worksheet->getFreezePane()) { $splitCell = $worksheet->getFreezePane() ?? ''; $topLeftCell = $worksheet->getTopLeftCell() ?? ''; - $splitCell = $this->updateCellReference($splitCell, $beforeCellAddress, $numberOfColumns, $numberOfRows); - $topLeftCell = $this->updateCellReference($topLeftCell, $beforeCellAddress, $numberOfColumns, $numberOfRows); + $splitCell = $this->updateCellReference($splitCell); + $topLeftCell = $this->updateCellReference($topLeftCell); $worksheet->freezePane($splitCell, $topLeftCell); } // Page setup if ($worksheet->getPageSetup()->isPrintAreaSet()) { - $worksheet->getPageSetup()->setPrintArea($this->updateCellReference($worksheet->getPageSetup()->getPrintArea(), $beforeCellAddress, $numberOfColumns, $numberOfRows)); + $worksheet->getPageSetup()->setPrintArea( + $this->updateCellReference($worksheet->getPageSetup()->getPrintArea()) + ); } // Update worksheet: drawings $aDrawings = $worksheet->getDrawingCollection(); foreach ($aDrawings as $objDrawing) { - $newReference = $this->updateCellReference($objDrawing->getCoordinates(), $beforeCellAddress, $numberOfColumns, $numberOfRows); + $newReference = $this->updateCellReference($objDrawing->getCoordinates()); if ($objDrawing->getCoordinates() != $newReference) { $objDrawing->setCoordinates($newReference); } + if ($objDrawing->getCoordinates2() !== '') { + $newReference = $this->updateCellReference($objDrawing->getCoordinates2()); + if ($objDrawing->getCoordinates2() != $newReference) { + $objDrawing->setCoordinates2($newReference); + } + } } // Update workbook: define names if (count($worksheet->getParent()->getDefinedNames()) > 0) { - foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { - if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { - $definedName->setValue($this->updateCellReference($definedName->getValue(), $beforeCellAddress, $numberOfColumns, $numberOfRows)); - } - } + $this->updateDefinedNames($worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); } // Garbage collect @@ -617,8 +556,21 @@ class ReferenceHelper * * @return string Updated formula */ - public function updateFormulaReferences($formula = '', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0, $worksheetName = '') - { + public function updateFormulaReferences( + $formula = '', + $beforeCellAddress = 'A1', + $numberOfColumns = 0, + $numberOfRows = 0, + $worksheetName = '', + bool $includeAbsoluteReferences = false + ) { + if ( + $this->cellReferenceHelper === null || + $this->cellReferenceHelper->refreshRequired($beforeCellAddress, $numberOfColumns, $numberOfRows) + ) { + $this->cellReferenceHelper = new CellReferenceHelper($beforeCellAddress, $numberOfColumns, $numberOfRows); + } + // Update cell references in the formula $formulaBlocks = explode('"', $formula); $i = false; @@ -628,13 +580,13 @@ class ReferenceHelper $adjustCount = 0; $newCellTokens = $cellTokens = []; // Search for row ranges (e.g. 'Sheet1'!3:5 or 3:5) with or without $ absolutes (e.g. $3:5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_ROWRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference('$A' . $match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows), 2); - $modified4 = substr($this->updateCellReference('$A' . $match[4], $beforeCellAddress, $numberOfColumns, $numberOfRows), 2); + $modified3 = substr($this->updateCellReference('$A' . $match[3], $includeAbsoluteReferences), 2); + $modified4 = substr($this->updateCellReference('$A' . $match[4], $includeAbsoluteReferences), 2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -653,13 +605,13 @@ class ReferenceHelper } } // Search for column ranges (e.g. 'Sheet1'!C:E or C:E) with or without $ absolutes (e.g. $C:E) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_COLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = substr($this->updateCellReference($match[3] . '$1', $beforeCellAddress, $numberOfColumns, $numberOfRows), 0, -2); - $modified4 = substr($this->updateCellReference($match[4] . '$1', $beforeCellAddress, $numberOfColumns, $numberOfRows), 0, -2); + $modified3 = substr($this->updateCellReference($match[3] . '$1', $includeAbsoluteReferences), 0, -2); + $modified4 = substr($this->updateCellReference($match[4] . '$1', $includeAbsoluteReferences), 0, -2); if ($match[3] . ':' . $match[4] !== $modified3 . ':' . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -678,13 +630,13 @@ class ReferenceHelper } } // Search for cell ranges (e.g. 'Sheet1'!A3:C5 or A3:C5) with or without $ absolutes (e.g. $A1:C$5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLRANGE . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3] . ':' . $match[4]; - $modified3 = $this->updateCellReference($match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows); - $modified4 = $this->updateCellReference($match[4], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); + $modified4 = $this->updateCellReference($match[4], $includeAbsoluteReferences); if ($match[3] . $match[4] !== $modified3 . $modified4) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { @@ -704,14 +656,14 @@ class ReferenceHelper } } // Search for cell references (e.g. 'Sheet1'!A3 or C5) with or without $ absolutes (e.g. $A1 or C$5) - $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/i', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); + $matchCount = preg_match_all('/' . self::REFHELPER_REGEXP_CELLREF . '/mui', ' ' . $formulaBlock . ' ', $matches, PREG_SET_ORDER); if ($matchCount > 0) { foreach ($matches as $match) { $fromString = ($match[2] > '') ? $match[2] . '!' : ''; $fromString .= $match[3]; - $modified3 = $this->updateCellReference($match[3], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $modified3 = $this->updateCellReference($match[3], $includeAbsoluteReferences); if ($match[3] !== $modified3) { if (($match[2] == '') || (trim($match[2], "'") == $worksheetName)) { $toString = ($match[2] > '') ? $match[2] . '!' : ''; @@ -739,7 +691,7 @@ class ReferenceHelper ksort($cellTokens); ksort($newCellTokens); } // Update cell references in the formula - $formulaBlock = str_replace('\\', '', preg_replace($cellTokens, $newCellTokens, $formulaBlock)); + $formulaBlock = str_replace('\\', '', (string) preg_replace($cellTokens, $newCellTokens, $formulaBlock)); } } } @@ -888,13 +840,10 @@ class ReferenceHelper * Update cell reference. * * @param string $cellReference Cell address or range of addresses - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment * * @return string Updated cell range */ - public function updateCellReference($cellReference = 'A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function updateCellReference($cellReference = 'A1', bool $includeAbsoluteReferences = false) { // Is it in another worksheet? Will not have to update anything. if (strpos($cellReference, '!') !== false) { @@ -902,10 +851,10 @@ class ReferenceHelper // Is it a range or a single cell? } elseif (!Coordinate::coordinateIsRange($cellReference)) { // Single cell - return $this->updateSingleCellReference($cellReference, $beforeCellAddress, $numberOfColumns, $numberOfRows); + return $this->cellReferenceHelper->updateCellReference($cellReference, $includeAbsoluteReferences); } elseif (Coordinate::coordinateIsRange($cellReference)) { // Range - return $this->updateCellRange($cellReference, $beforeCellAddress, $numberOfColumns, $numberOfRows); + return $this->updateCellRange($cellReference, $includeAbsoluteReferences); } // Return original @@ -913,13 +862,13 @@ class ReferenceHelper } /** - * Update named formulas (i.e. containing worksheet references / named ranges). + * Update named formulae (i.e. containing worksheet references / named ranges). * * @param Spreadsheet $spreadsheet Object to update * @param string $oldName Old name (name to replace) * @param string $newName New name */ - public function updateNamedFormulas(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void + public function updateNamedFormulae(Spreadsheet $spreadsheet, $oldName = '', $newName = ''): void { if ($oldName == '') { return; @@ -928,7 +877,7 @@ class ReferenceHelper foreach ($spreadsheet->getWorksheetIterator() as $sheet) { foreach ($sheet->getCoordinates(false) as $coordinate) { $cell = $sheet->getCell($coordinate); - if (($cell !== null) && ($cell->getDataType() == DataType::TYPE_FORMULA)) { + if ($cell->getDataType() === DataType::TYPE_FORMULA) { $formula = $cell->getValue(); if (strpos($formula, $oldName) !== false) { $formula = str_replace("'" . $oldName . "'!", "'" . $newName . "'!", $formula); @@ -940,17 +889,48 @@ class ReferenceHelper } } + private function updateDefinedNames(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + foreach ($worksheet->getParent()->getDefinedNames() as $definedName) { + if ($definedName->isFormula() === false) { + $this->updateNamedRange($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } else { + $this->updateNamedFormula($definedName, $worksheet, $beforeCellAddress, $numberOfColumns, $numberOfRows); + } + } + } + + private function updateNamedRange(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + $cellAddress = $definedName->getValue(); + $asFormula = ($cellAddress[0] === '='); + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + if ($asFormula === true) { + $formula = $this->updateFormulaReferences($cellAddress, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } else { + $definedName->setValue($this->updateCellReference(ltrim($cellAddress, '='))); + } + } + } + + private function updateNamedFormula(DefinedName $definedName, Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns, int $numberOfRows): void + { + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $worksheet->getHashCode()) { + $formula = $definedName->getValue(); + $formula = $this->updateFormulaReferences($formula, $beforeCellAddress, $numberOfColumns, $numberOfRows, $worksheet->getTitle()); + $definedName->setValue($formula); + } + } + /** * Update cell range. * * @param string $cellRange Cell range (e.g. 'B2:D4', 'B:C' or '2:3') - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment * * @return string Updated cell range */ - private function updateCellRange($cellRange = 'A1:A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function updateCellRange(string $cellRange = 'A1:A1', bool $includeAbsoluteReferences = false): string { if (!Coordinate::coordinateIsRange($cellRange)) { throw new Exception('Only cell ranges may be passed to this method.'); @@ -963,13 +943,15 @@ class ReferenceHelper $jc = count($range[$i]); for ($j = 0; $j < $jc; ++$j) { if (ctype_alpha($range[$i][$j])) { - $r = Coordinate::coordinateFromString($this->updateSingleCellReference($range[$i][$j] . '1', $beforeCellAddress, $numberOfColumns, $numberOfRows)); - $range[$i][$j] = $r[0]; + $range[$i][$j] = Coordinate::coordinateFromString( + $this->cellReferenceHelper->updateCellReference($range[$i][$j] . '1', $includeAbsoluteReferences) + )[0]; } elseif (ctype_digit($range[$i][$j])) { - $r = Coordinate::coordinateFromString($this->updateSingleCellReference('A' . $range[$i][$j], $beforeCellAddress, $numberOfColumns, $numberOfRows)); - $range[$i][$j] = $r[1]; + $range[$i][$j] = Coordinate::coordinateFromString( + $this->cellReferenceHelper->updateCellReference('A' . $range[$i][$j], $includeAbsoluteReferences) + )[1]; } else { - $range[$i][$j] = $this->updateSingleCellReference($range[$i][$j], $beforeCellAddress, $numberOfColumns, $numberOfRows); + $range[$i][$j] = $this->cellReferenceHelper->updateCellReference($range[$i][$j], $includeAbsoluteReferences); } } } @@ -978,44 +960,230 @@ class ReferenceHelper return Coordinate::buildRange($range); } - /** - * Update single cell reference. - * - * @param string $cellReference Single cell reference - * @param string $beforeCellAddress Insert before this one - * @param int $numberOfColumns Number of columns to increment - * @param int $numberOfRows Number of rows to increment - * - * @return string Updated cell reference - */ - private function updateSingleCellReference($cellReference = 'A1', $beforeCellAddress = 'A1', $numberOfColumns = 0, $numberOfRows = 0) + private function clearColumnStrips(int $highestRow, int $beforeColumn, int $numberOfColumns, Worksheet $worksheet): void { - if (Coordinate::coordinateIsRange($cellReference)) { - throw new Exception('Only single cell references may be passed to this method.'); + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn + $numberOfColumns); + $endColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + + for ($row = 1; $row <= $highestRow - 1; ++$row) { + for ($column = $startColumnId; $column !== $endColumnId; ++$column) { + $coordinate = $column . $row; + $this->clearStripCell($worksheet, $coordinate); + } } + } - // Get coordinate of $beforeCellAddress - [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($beforeCellAddress); + private function clearRowStrips(string $highestColumn, int $beforeColumn, int $beforeRow, int $numberOfRows, Worksheet $worksheet): void + { + $startColumnId = Coordinate::stringFromColumnIndex($beforeColumn); + ++$highestColumn; - // Get coordinate of $cellReference - [$newColumn, $newRow] = Coordinate::coordinateFromString($cellReference); - - // Verify which parts should be updated - $updateColumn = (($newColumn[0] != '$') && ($beforeColumn[0] != '$') && (Coordinate::columnIndexFromString($newColumn) >= Coordinate::columnIndexFromString($beforeColumn))); - $updateRow = (($newRow[0] != '$') && ($beforeRow[0] != '$') && $newRow >= $beforeRow); - - // Create new column reference - if ($updateColumn) { - $newColumn = Coordinate::stringFromColumnIndex(Coordinate::columnIndexFromString($newColumn) + $numberOfColumns); + for ($column = $startColumnId; $column !== $highestColumn; ++$column) { + for ($row = $beforeRow + $numberOfRows; $row <= $beforeRow - 1; ++$row) { + $coordinate = $column . $row; + $this->clearStripCell($worksheet, $coordinate); + } } + } - // Create new row reference - if ($updateRow) { - $newRow = (int) $newRow + $numberOfRows; + private function clearStripCell(Worksheet $worksheet, string $coordinate): void + { + $worksheet->removeConditionalStyles($coordinate); + $worksheet->setHyperlink($coordinate); + $worksheet->setDataValidation($coordinate); + $worksheet->removeComment($coordinate); + + if ($worksheet->cellExists($coordinate)) { + $worksheet->getCell($coordinate)->setValueExplicit(null, DataType::TYPE_NULL); + $worksheet->getCell($coordinate)->setXfIndex(0); } + } - // Return new reference - return $newColumn . $newRow; + private function adjustAutoFilter(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void + { + $autoFilter = $worksheet->getAutoFilter(); + $autoFilterRange = $autoFilter->getRange(); + if (!empty($autoFilterRange)) { + if ($numberOfColumns !== 0) { + $autoFilterColumns = $autoFilter->getColumns(); + if (count($autoFilterColumns) > 0) { + $column = ''; + $row = 0; + sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString($column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($autoFilterRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($numberOfColumns < 0) { + $this->adjustAutoFilterDeleteRules($columnIndex, $numberOfColumns, $autoFilterColumns, $autoFilter); + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in autofilter range + if ($numberOfColumns > 0) { + $this->adjustAutoFilterInsert($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); + } else { + $this->adjustAutoFilterDelete($startCol, $numberOfColumns, $rangeEnd[0], $autoFilter); + } + } + } + } + + $worksheet->setAutoFilter( + $this->updateCellReference($autoFilterRange) + ); + } + } + + private function adjustAutoFilterDeleteRules(int $columnIndex, int $numberOfColumns, array $autoFilterColumns, AutoFilter $autoFilter): void + { + // If we're actually deleting any columns that fall within the autofilter range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + + for ($i = 1; $i <= $deleteCount; ++$i) { + $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); + if (isset($autoFilterColumns[$columnName])) { + $autoFilter->clearColumn($columnName); + } + ++$deleteColumn; + } + } + + private function adjustAutoFilterInsert(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + $startColRef = $startCol; + $endColRef = $rangeEnd; + $toColRef = $rangeEnd + $numberOfColumns; + + do { + $autoFilter->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } + + private function adjustAutoFilterDelete(int $startCol, int $numberOfColumns, int $rangeEnd, AutoFilter $autoFilter): void + { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); + + do { + $autoFilter->shiftColumn($startColID, $toColID); + ++$startColID; + ++$toColID; + } while ($startColID !== $endColID); + } + + private function adjustTable(Worksheet $worksheet, string $beforeCellAddress, int $numberOfColumns): void + { + $tableCollection = $worksheet->getTableCollection(); + + foreach ($tableCollection as $table) { + $tableRange = $table->getRange(); + if (!empty($tableRange)) { + if ($numberOfColumns !== 0) { + $tableColumns = $table->getColumns(); + if (count($tableColumns) > 0) { + $column = ''; + $row = 0; + sscanf($beforeCellAddress, '%[A-Z]%d', $column, $row); + $columnIndex = Coordinate::columnIndexFromString($column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($tableRange); + if ($columnIndex <= $rangeEnd[0]) { + if ($numberOfColumns < 0) { + $this->adjustTableDeleteRules($columnIndex, $numberOfColumns, $tableColumns, $table); + } + $startCol = ($columnIndex > $rangeStart[0]) ? $columnIndex : $rangeStart[0]; + + // Shuffle columns in table range + if ($numberOfColumns > 0) { + $this->adjustTableInsert($startCol, $numberOfColumns, $rangeEnd[0], $table); + } else { + $this->adjustTableDelete($startCol, $numberOfColumns, $rangeEnd[0], $table); + } + } + } + } + + $table->setRange($this->updateCellReference($tableRange)); + } + } + } + + private function adjustTableDeleteRules(int $columnIndex, int $numberOfColumns, array $tableColumns, Table $table): void + { + // If we're actually deleting any columns that fall within the table range, + // then we delete any rules for those columns + $deleteColumn = $columnIndex + $numberOfColumns - 1; + $deleteCount = abs($numberOfColumns); + + for ($i = 1; $i <= $deleteCount; ++$i) { + $columnName = Coordinate::stringFromColumnIndex($deleteColumn + 1); + if (isset($tableColumns[$columnName])) { + $table->clearColumn($columnName); + } + ++$deleteColumn; + } + } + + private function adjustTableInsert(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void + { + $startColRef = $startCol; + $endColRef = $rangeEnd; + $toColRef = $rangeEnd + $numberOfColumns; + + do { + $table->shiftColumn(Coordinate::stringFromColumnIndex($endColRef), Coordinate::stringFromColumnIndex($toColRef)); + --$endColRef; + --$toColRef; + } while ($startColRef <= $endColRef); + } + + private function adjustTableDelete(int $startCol, int $numberOfColumns, int $rangeEnd, Table $table): void + { + // For delete, we shuffle from beginning to end to avoid overwriting + $startColID = Coordinate::stringFromColumnIndex($startCol); + $toColID = Coordinate::stringFromColumnIndex($startCol + $numberOfColumns); + $endColID = Coordinate::stringFromColumnIndex($rangeEnd + 1); + + do { + $table->shiftColumn($startColID, $toColID); + ++$startColID; + ++$toColID; + } while ($startColID !== $endColID); + } + + private function duplicateStylesByColumn(Worksheet $worksheet, int $beforeColumn, int $beforeRow, int $highestRow, int $numberOfColumns): void + { + $beforeColumnName = Coordinate::stringFromColumnIndex($beforeColumn - 1); + for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { + // Style + $coordinate = $beforeColumnName . $i; + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $numberOfColumns; ++$j) { + $worksheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); + } + } + } + } + + private function duplicateStylesByRow(Worksheet $worksheet, int $beforeColumn, int $beforeRow, string $highestColumn, int $numberOfRows): void + { + $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); + for ($i = $beforeColumn; $i <= $highestColumnIndex; ++$i) { + // Style + $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); + if ($worksheet->cellExists($coordinate)) { + $xfIndex = $worksheet->getCell($coordinate)->getXfIndex(); + for ($j = $beforeRow; $j <= $beforeRow - 1 + $numberOfRows; ++$j) { + $worksheet->getCell(Coordinate::stringFromColumnIndex($i) . $j)->setXfIndex($xfIndex); + } + } + } } /** diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/RichText.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/RichText.php index 3a6e1f8e350..88e7c79236b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/RichText.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/RichText.php @@ -158,11 +158,14 @@ class RichText implements IComparable { $vars = get_object_vars($this); foreach ($vars as $key => $value) { - if (is_object($value)) { - $this->$key = clone $value; - } else { - $this->$key = $value; + $newValue = is_object($value) ? (clone $value) : $value; + if (is_array($value)) { + $newValue = []; + foreach ($value as $key2 => $value2) { + $newValue[$key2] = is_object($value2) ? (clone $value2) : $value2; + } } + $this->$key = $newValue; } } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/TextElement.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/TextElement.php index 6bec005b4d5..23733436bb5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/TextElement.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/RichText/TextElement.php @@ -68,19 +68,4 @@ class TextElement implements ITextElement __CLASS__ ); } - - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ - public function __clone() - { - $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if (is_object($value)) { - $this->$key = clone $value; - } else { - $this->$key = $value; - } - } - } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php index 5fbbadb6764..3282a596eb9 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Settings.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Collection\Memory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; use Psr\SimpleCache\CacheInterface; +use ReflectionClass; class Settings { @@ -161,12 +162,19 @@ class Settings public static function getCache(): CacheInterface { if (!self::$cache) { - self::$cache = new Memory(); + self::$cache = self::useSimpleCacheVersion3() ? new Memory\SimpleCache3() : new Memory\SimpleCache1(); } return self::$cache; } + public static function useSimpleCacheVersion3(): bool + { + return + PHP_MAJOR_VERSION === 8 && + (new ReflectionClass(CacheInterface::class))->getMethod('get')->getReturnType() !== null; + } + /** * Set the HTTP client implementation to be used for network request. */ diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php index 5b0a2907a1f..872780737f2 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Date.php @@ -7,8 +7,11 @@ use DateTimeInterface; use DateTimeZone; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; +use PhpOffice\PhpSpreadsheet\Shared\Date as SharedDate; use PhpOffice\PhpSpreadsheet\Style\NumberFormat; class Date @@ -157,6 +160,36 @@ class Date throw new PhpSpreadsheetException('Invalid timezone'); } + /** + * @param mixed $value + * + * @return float|int + */ + public static function convertIsoDate($value) + { + if (!is_string($value)) { + throw new Exception('Non-string value supplied for Iso Date conversion'); + } + + $date = new DateTime($value); + $dateErrors = DateTime::getLastErrors(); + + if (is_array($dateErrors) && ($dateErrors['warning_count'] > 0 || $dateErrors['error_count'] > 0)) { + throw new Exception("Invalid string $value supplied for datatype Date"); + } + + $newValue = SharedDate::PHPToExcel($date); + if ($newValue === false) { + throw new Exception("Invalid string $value supplied for datatype Date"); + } + + if (preg_match('/^\\d\\d:\\d\\d:\\d\\d/', $value) == 1) { + $newValue = fmod($newValue, 1.0); + } + + return $newValue; + } + /** * Convert a MS serialized datetime value from Excel to a PHP Date/Time object. * @@ -228,7 +261,7 @@ class Date * @param mixed $dateValue PHP DateTime object or a string - Unix timestamp is also permitted, but discouraged; * not Y2038-safe on a 32-bit system, and no timezone info * - * @return bool|float Excel date/time value + * @return false|float Excel date/time value * or boolean FALSE on failure */ public static function PHPToExcel($dateValue) @@ -454,13 +487,13 @@ class Date $dateValueNew = DateTimeExcel\DateValue::fromString($dateValue); - if ($dateValueNew === Functions::VALUE()) { + if ($dateValueNew === ExcelError::VALUE()) { return false; } if (strpos($dateValue, ':') !== false) { $timeValue = DateTimeExcel\TimeValue::fromString($dateValue); - if ($timeValue === Functions::VALUE()) { + if ($timeValue === ExcelError::VALUE()) { return false; } $dateValueNew += $timeValue; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Drawing.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Drawing.php index 0d8ad618ba2..3378958c83b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Drawing.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Drawing.php @@ -164,11 +164,15 @@ class Drawing { // Load the image into a string $file = fopen($bmpFilename, 'rb'); + /** @phpstan-ignore-next-line */ $read = fread($file, 10); + // @phpstan-ignore-next-line while (!feof($file) && ($read != '')) { + // @phpstan-ignore-next-line $read .= fread($file, 1024); } + /** @phpstan-ignore-next-line */ $temp = unpack('H*', $read); $hex = $temp[1]; $header = substr($hex, 0, 108); @@ -196,6 +200,8 @@ class Drawing $y = 1; // Create newimage + + /** @phpstan-ignore-next-line */ $image = imagecreatetruecolor($width, $height); // Grab the body from the image @@ -241,7 +247,10 @@ class Drawing $b = hexdec($body[$i_pos] . $body[$i_pos + 1]); // Calculate and draw the pixel + + /** @phpstan-ignore-next-line */ $color = imagecolorallocate($image, $r, $g, $b); + // @phpstan-ignore-next-line imagesetpixel($image, $x, $height - $y, $color); // Raise the horizontal position @@ -252,6 +261,7 @@ class Drawing unset($body); // Return image-object + // @phpstan-ignore-next-line return $image; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php index 63ce0e036a6..f2fe8caa837 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/File.php @@ -114,12 +114,6 @@ class File */ public static function sysGetTempDir(): string { - // Moodle hack! - if (function_exists('make_temp_directory')) { - $temp = make_temp_directory('phpspreadsheet'); - return realpath(dirname($temp)); - } - $path = sys_get_temp_dir(); if (self::$useUploadTempDirectory) { // use upload-directory when defined to allow running on environments having very restricted diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php index 9a74befeb9b..e90c679b6c5 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Font.php @@ -13,7 +13,7 @@ class Font const AUTOSIZE_METHOD_APPROX = 'approx'; const AUTOSIZE_METHOD_EXACT = 'exact'; - private static $autoSizeMethods = [ + private const AUTOSIZE_METHODS = [ self::AUTOSIZE_METHOD_APPROX, self::AUTOSIZE_METHOD_EXACT, ]; @@ -101,6 +101,105 @@ class Font const VERDANA_ITALIC = 'verdanai.ttf'; const VERDANA_BOLD_ITALIC = 'verdanaz.ttf'; + const FONT_FILE_NAMES = [ + 'Arial' => [ + 'x' => self::ARIAL, + 'xb' => self::ARIAL_BOLD, + 'xi' => self::ARIAL_ITALIC, + 'xbi' => self::ARIAL_BOLD_ITALIC, + ], + 'Calibri' => [ + 'x' => self::CALIBRI, + 'xb' => self::CALIBRI_BOLD, + 'xi' => self::CALIBRI_ITALIC, + 'xbi' => self::CALIBRI_BOLD_ITALIC, + ], + 'Comic Sans MS' => [ + 'x' => self::COMIC_SANS_MS, + 'xb' => self::COMIC_SANS_MS_BOLD, + 'xi' => self::COMIC_SANS_MS, + 'xbi' => self::COMIC_SANS_MS_BOLD, + ], + 'Courier New' => [ + 'x' => self::COURIER_NEW, + 'xb' => self::COURIER_NEW_BOLD, + 'xi' => self::COURIER_NEW_ITALIC, + 'xbi' => self::COURIER_NEW_BOLD_ITALIC, + ], + 'Georgia' => [ + 'x' => self::GEORGIA, + 'xb' => self::GEORGIA_BOLD, + 'xi' => self::GEORGIA_ITALIC, + 'xbi' => self::GEORGIA_BOLD_ITALIC, + ], + 'Impact' => [ + 'x' => self::IMPACT, + 'xb' => self::IMPACT, + 'xi' => self::IMPACT, + 'xbi' => self::IMPACT, + ], + 'Liberation Sans' => [ + 'x' => self::LIBERATION_SANS, + 'xb' => self::LIBERATION_SANS_BOLD, + 'xi' => self::LIBERATION_SANS_ITALIC, + 'xbi' => self::LIBERATION_SANS_BOLD_ITALIC, + ], + 'Lucida Console' => [ + 'x' => self::LUCIDA_CONSOLE, + 'xb' => self::LUCIDA_CONSOLE, + 'xi' => self::LUCIDA_CONSOLE, + 'xbi' => self::LUCIDA_CONSOLE, + ], + 'Lucida Sans Unicode' => [ + 'x' => self::LUCIDA_SANS_UNICODE, + 'xb' => self::LUCIDA_SANS_UNICODE, + 'xi' => self::LUCIDA_SANS_UNICODE, + 'xbi' => self::LUCIDA_SANS_UNICODE, + ], + 'Microsoft Sans Serif' => [ + 'x' => self::MICROSOFT_SANS_SERIF, + 'xb' => self::MICROSOFT_SANS_SERIF, + 'xi' => self::MICROSOFT_SANS_SERIF, + 'xbi' => self::MICROSOFT_SANS_SERIF, + ], + 'Palatino Linotype' => [ + 'x' => self::PALATINO_LINOTYPE, + 'xb' => self::PALATINO_LINOTYPE_BOLD, + 'xi' => self::PALATINO_LINOTYPE_ITALIC, + 'xbi' => self::PALATINO_LINOTYPE_BOLD_ITALIC, + ], + 'Symbol' => [ + 'x' => self::SYMBOL, + 'xb' => self::SYMBOL, + 'xi' => self::SYMBOL, + 'xbi' => self::SYMBOL, + ], + 'Tahoma' => [ + 'x' => self::TAHOMA, + 'xb' => self::TAHOMA_BOLD, + 'xi' => self::TAHOMA, + 'xbi' => self::TAHOMA_BOLD, + ], + 'Times New Roman' => [ + 'x' => self::TIMES_NEW_ROMAN, + 'xb' => self::TIMES_NEW_ROMAN_BOLD, + 'xi' => self::TIMES_NEW_ROMAN_ITALIC, + 'xbi' => self::TIMES_NEW_ROMAN_BOLD_ITALIC, + ], + 'Trebuchet MS' => [ + 'x' => self::TREBUCHET_MS, + 'xb' => self::TREBUCHET_MS_BOLD, + 'xi' => self::TREBUCHET_MS_ITALIC, + 'xbi' => self::TREBUCHET_MS_BOLD_ITALIC, + ], + 'Verdana' => [ + 'x' => self::VERDANA, + 'xb' => self::VERDANA_BOLD, + 'xi' => self::VERDANA_ITALIC, + 'xbi' => self::VERDANA_BOLD_ITALIC, + ], + ]; + /** * AutoSize method. * @@ -113,54 +212,65 @@ class Font * * @var string */ - private static $trueTypeFontPath; + private static $trueTypeFontPath = ''; /** * How wide is a default column for a given default font and size? * Empirical data found by inspecting real Excel files and reading off the pixel width * in Microsoft Office Excel 2007. + * Added height in points. + */ + public const DEFAULT_COLUMN_WIDTHS = [ + 'Arial' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + ], + 'Calibri' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.00], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 56, 'width' => 9.33203125, 'height' => 11.25], + 9 => ['px' => 56, 'width' => 9.33203125, 'height' => 12.0], + 10 => ['px' => 64, 'width' => 9.14062500, 'height' => 12.75], + 11 => ['px' => 64, 'width' => 9.14062500, 'height' => 15.0], + ], + 'Verdana' => [ + 1 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 2 => ['px' => 24, 'width' => 12.00000000, 'height' => 5.25], + 3 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.0], + 4 => ['px' => 32, 'width' => 10.66406250, 'height' => 6.75], + 5 => ['px' => 40, 'width' => 10.00000000, 'height' => 8.25], + 6 => ['px' => 48, 'width' => 9.59765625, 'height' => 8.25], + 7 => ['px' => 48, 'width' => 9.59765625, 'height' => 9.0], + 8 => ['px' => 64, 'width' => 9.14062500, 'height' => 10.5], + 9 => ['px' => 72, 'width' => 9.00000000, 'height' => 11.25], + 10 => ['px' => 72, 'width' => 9.00000000, 'height' => 12.75], + ], + ]; + + /** + * List of column widths. Replaced by constant; + * previously it was public and updateable, allowing + * user to make inappropriate alterations. + * + * @deprecated 1.25.0 Use DEFAULT_COLUMN_WIDTHS constant instead. * * @var array */ - public static $defaultColumnWidths = [ - 'Arial' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 64, 'width' => 9.14062500], - 10 => ['px' => 64, 'width' => 9.14062500], - ], - 'Calibri' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 56, 'width' => 9.33203125], - 9 => ['px' => 56, 'width' => 9.33203125], - 10 => ['px' => 64, 'width' => 9.14062500], - 11 => ['px' => 64, 'width' => 9.14062500], - ], - 'Verdana' => [ - 1 => ['px' => 24, 'width' => 12.00000000], - 2 => ['px' => 24, 'width' => 12.00000000], - 3 => ['px' => 32, 'width' => 10.66406250], - 4 => ['px' => 32, 'width' => 10.66406250], - 5 => ['px' => 40, 'width' => 10.00000000], - 6 => ['px' => 48, 'width' => 9.59765625], - 7 => ['px' => 48, 'width' => 9.59765625], - 8 => ['px' => 64, 'width' => 9.14062500], - 9 => ['px' => 72, 'width' => 9.00000000], - 10 => ['px' => 72, 'width' => 9.00000000], - ], - ]; + public static $defaultColumnWidths = self::DEFAULT_COLUMN_WIDTHS; /** * Set autoSize method. @@ -171,7 +281,7 @@ class Font */ public static function setAutoSizeMethod($method) { - if (!in_array($method, self::$autoSizeMethods)) { + if (!in_array($method, self::AUTOSIZE_METHODS)) { return false; } self::$autoSizeMethod = $method; @@ -219,35 +329,47 @@ class Font * Calculate an (approximate) OpenXML column width, based on font size and text contained. * * @param FontStyle $font Font object - * @param RichText|string $cellText Text to calculate width + * @param null|RichText|string $cellText Text to calculate width * @param int $rotation Rotation angle * @param null|FontStyle $defaultFont Font object - * - * @return int Column width + * @param bool $filterAdjustment Add space for Autofilter or Table dropdown */ - public static function calculateColumnWidth(FontStyle $font, $cellText = '', $rotation = 0, ?FontStyle $defaultFont = null) - { + public static function calculateColumnWidth( + FontStyle $font, + $cellText = '', + $rotation = 0, + ?FontStyle $defaultFont = null, + bool $filterAdjustment = false, + int $indentAdjustment = 0 + ): int { // If it is rich text, use plain text if ($cellText instanceof RichText) { $cellText = $cellText->getPlainText(); } // Special case if there are one or more newline characters ("\n") - if (strpos($cellText ?? '', "\n") !== false) { + $cellText = $cellText ?? ''; + if (strpos(/** @scrutinizer ignore-type */ $cellText, "\n") !== false) { $lineTexts = explode("\n", $cellText); $lineWidths = []; foreach ($lineTexts as $lineText) { - $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont); + $lineWidths[] = self::calculateColumnWidth($font, $lineText, $rotation = 0, $defaultFont, $filterAdjustment); } return max($lineWidths); // width of longest line in cell } // Try to get the exact text width in pixels - $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX; + $approximate = self::$autoSizeMethod === self::AUTOSIZE_METHOD_APPROX; $columnWidth = 0; if (!$approximate) { - $columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07); + $columnWidthAdjust = ceil( + self::getTextWidthPixelsExact( + str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))), + $font, + 0 + ) * 1.07 + ); try { // Width of text in pixels excl. padding @@ -259,14 +381,18 @@ class Font } if ($approximate) { - $columnWidthAdjust = self::getTextWidthPixelsApprox('n', $font, 0); + $columnWidthAdjust = self::getTextWidthPixelsApprox( + str_repeat('n', 1 * (($filterAdjustment ? 3 : 1) + ($indentAdjustment * 2))), + $font, + 0 + ); // Width of text in pixels excl. padding, approximation // and addition because Excel adds some padding, just use approx width of 'n' glyph $columnWidth = self::getTextWidthPixelsApprox($cellText, $font, $rotation) + $columnWidthAdjust; } // Convert from pixel width to column width - $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont); + $columnWidth = Drawing::pixelsToCellDimension((int) $columnWidth, $defaultFont ?? new FontStyle()); // Return return (int) round($columnWidth, 6); @@ -284,7 +410,12 @@ class Font // font size should really be supplied in pixels in GD2, // but since GD2 seems to assume 72dpi, pixels and points are the same $fontFile = self::getTrueTypeFontFileFromFont($font); - $textBox = imagettfbbox($font->getSize(), $rotation, $fontFile, $text); + $textBox = imagettfbbox($font->getSize() ?? 10.0, $rotation, $fontFile, $text); + if ($textBox === false) { + // @codeCoverageIgnoreStart + throw new PhpSpreadsheetException('imagettfbbox failed'); + // @codeCoverageIgnoreEnd + } // Get corners positions $lowerLeftCornerX = $textBox[0]; @@ -394,129 +525,48 @@ class Font * * @return string Path to TrueType font file */ - public static function getTrueTypeFontFileFromFont(FontStyle $font) + public static function getTrueTypeFontFileFromFont(FontStyle $font, bool $checkPath = true) { - if (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath)) { + if ($checkPath && (!file_exists(self::$trueTypeFontPath) || !is_dir(self::$trueTypeFontPath))) { throw new PhpSpreadsheetException('Valid directory to TrueType Font files not specified'); } $name = $font->getName(); + if (!isset(self::FONT_FILE_NAMES[$name])) { + throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); + } $bold = $font->getBold(); $italic = $font->getItalic(); - - // Check if we can map font to true type font file - switch ($name) { - case 'Arial': - $fontFile = ( - $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD) - : ($italic ? self::ARIAL_ITALIC : self::ARIAL) - ); - - break; - case 'Calibri': - $fontFile = ( - $bold ? ($italic ? self::CALIBRI_BOLD_ITALIC : self::CALIBRI_BOLD) - : ($italic ? self::CALIBRI_ITALIC : self::CALIBRI) - ); - - break; - case 'Courier New': - $fontFile = ( - $bold ? ($italic ? self::COURIER_NEW_BOLD_ITALIC : self::COURIER_NEW_BOLD) - : ($italic ? self::COURIER_NEW_ITALIC : self::COURIER_NEW) - ); - - break; - case 'Comic Sans MS': - $fontFile = ( - $bold ? self::COMIC_SANS_MS_BOLD : self::COMIC_SANS_MS - ); - - break; - case 'Georgia': - $fontFile = ( - $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD) - : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA) - ); - - break; - case 'Impact': - $fontFile = self::IMPACT; - - break; - case 'Liberation Sans': - $fontFile = ( - $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) - : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) - ); - - break; - case 'Lucida Console': - $fontFile = self::LUCIDA_CONSOLE; - - break; - case 'Lucida Sans Unicode': - $fontFile = self::LUCIDA_SANS_UNICODE; - - break; - case 'Microsoft Sans Serif': - $fontFile = self::MICROSOFT_SANS_SERIF; - - break; - case 'Palatino Linotype': - $fontFile = ( - $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) - : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) - ); - - break; - case 'Symbol': - $fontFile = self::SYMBOL; - - break; - case 'Tahoma': - $fontFile = ( - $bold ? self::TAHOMA_BOLD : self::TAHOMA - ); - - break; - case 'Times New Roman': - $fontFile = ( - $bold ? ($italic ? self::TIMES_NEW_ROMAN_BOLD_ITALIC : self::TIMES_NEW_ROMAN_BOLD) - : ($italic ? self::TIMES_NEW_ROMAN_ITALIC : self::TIMES_NEW_ROMAN) - ); - - break; - case 'Trebuchet MS': - $fontFile = ( - $bold ? ($italic ? self::TREBUCHET_MS_BOLD_ITALIC : self::TREBUCHET_MS_BOLD) - : ($italic ? self::TREBUCHET_MS_ITALIC : self::TREBUCHET_MS) - ); - - break; - case 'Verdana': - $fontFile = ( - $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD) - : ($italic ? self::VERDANA_ITALIC : self::VERDANA) - ); - - break; - default: - throw new PhpSpreadsheetException('Unknown font name "' . $name . '". Cannot map to TrueType font file'); - - break; + $index = 'x'; + if ($bold) { + $index .= 'b'; } + if ($italic) { + $index .= 'i'; + } + $fontFile = self::FONT_FILE_NAMES[$name][$index]; - $fontFile = self::$trueTypeFontPath . $fontFile; + $separator = ''; + if (mb_strlen(self::$trueTypeFontPath) > 1 && mb_substr(self::$trueTypeFontPath, -1) !== '/' && mb_substr(self::$trueTypeFontPath, -1) !== '\\') { + $separator = DIRECTORY_SEPARATOR; + } + $fontFile = self::$trueTypeFontPath . $separator . $fontFile; // Check if file actually exists - if (!file_exists($fontFile)) { + if ($checkPath && !file_exists($fontFile)) { throw new PhpSpreadsheetException('TrueType Font file not found'); } return $fontFile; } + public const CHARSET_FROM_FONT_NAME = [ + 'EucrosiaUPC' => self::CHARSET_ANSI_THAI, + 'Wingdings' => self::CHARSET_SYMBOL, + 'Wingdings 2' => self::CHARSET_SYMBOL, + 'Wingdings 3' => self::CHARSET_SYMBOL, + ]; + /** * Returns the associated charset for the font name. * @@ -526,19 +576,7 @@ class Font */ public static function getCharsetFromFontName($fontName) { - switch ($fontName) { - // Add more cases. Check FONT records in real Excel files. - case 'EucrosiaUPC': - return self::CHARSET_ANSI_THAI; - case 'Wingdings': - return self::CHARSET_SYMBOL; - case 'Wingdings 2': - return self::CHARSET_SYMBOL; - case 'Wingdings 3': - return self::CHARSET_SYMBOL; - default: - return self::CHARSET_ANSI_LATIN; - } + return self::CHARSET_FROM_FONT_NAME[$fontName] ?? self::CHARSET_ANSI_LATIN; } /** @@ -552,17 +590,17 @@ class Font */ public static function getDefaultColumnWidthByFont(FontStyle $font, $returnAsPixels = false) { - if (isset(self::$defaultColumnWidths[$font->getName()][$font->getSize()])) { + if (isset(self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()])) { // Exact width can be determined $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px'] - : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width']; + self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['px'] + : self::DEFAULT_COLUMN_WIDTHS[$font->getName()][$font->getSize()]['width']; } else { // We don't have data for this particular font and size, use approximation by // extrapolating from Calibri 11 $columnWidth = $returnAsPixels ? - self::$defaultColumnWidths['Calibri'][11]['px'] - : self::$defaultColumnWidths['Calibri'][11]['width']; + self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['px'] + : self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['width']; $columnWidth = $columnWidth * $font->getSize() / 11; // Round pixels to closest integer @@ -584,173 +622,14 @@ class Font */ public static function getDefaultRowHeightByFont(FontStyle $font) { - switch ($font->getName()) { - case 'Arial': - switch ($font->getSize()) { - case 10: - // inspection of Arial 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Arial 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Arial 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Arial 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Arial 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Arial 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Arial 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Arial 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Arial 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - case 'Calibri': - switch ($font->getSize()) { - case 11: - // inspection of Calibri 11 workbook says 15.00pt ~20px - $rowHeight = 15; - - break; - case 10: - // inspection of Calibri 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Calibri 9 workbook says 12.00pt ~16px - $rowHeight = 12; - - break; - case 8: - // inspection of Calibri 8 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 7: - // inspection of Calibri 7 workbook says 9.00pt ~12px - $rowHeight = 9; - - break; - case 6: - case 5: - // inspection of Calibri 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Calibri 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Calibri 3 workbook says 6.00pt ~8px - $rowHeight = 6.00; - - break; - case 2: - case 1: - // inspection of Calibri 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Calibri 11 workbook as an approximation, extrapolation - $rowHeight = 15 * $font->getSize() / 11; - - break; - } - - break; - case 'Verdana': - switch ($font->getSize()) { - case 10: - // inspection of Verdana 10 workbook says 12.75pt ~17px - $rowHeight = 12.75; - - break; - case 9: - // inspection of Verdana 9 workbook says 11.25pt ~15px - $rowHeight = 11.25; - - break; - case 8: - // inspection of Verdana 8 workbook says 10.50pt ~14px - $rowHeight = 10.50; - - break; - case 7: - // inspection of Verdana 7 workbook says 9.00pt ~12px - $rowHeight = 9.00; - - break; - case 6: - case 5: - // inspection of Verdana 5,6 workbook says 8.25pt ~11px - $rowHeight = 8.25; - - break; - case 4: - // inspection of Verdana 4 workbook says 6.75pt ~9px - $rowHeight = 6.75; - - break; - case 3: - // inspection of Verdana 3 workbook says 6.00pt ~8px - $rowHeight = 6; - - break; - case 2: - case 1: - // inspection of Verdana 1,2 workbook says 5.25pt ~7px - $rowHeight = 5.25; - - break; - default: - // use Verdana 10 workbook as an approximation, extrapolation - $rowHeight = 12.75 * $font->getSize() / 10; - - break; - } - - break; - default: - // just use Calibri as an approximation - $rowHeight = 15 * $font->getSize() / 11; - - break; + $name = $font->getName(); + $size = $font->getSize(); + if (isset(self::DEFAULT_COLUMN_WIDTHS[$name][$size])) { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][$size]['height']; + } elseif ($name === 'Arial' || $name === 'Verdana') { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS[$name][10]['height'] * $size / 10.0; + } else { + $rowHeight = self::DEFAULT_COLUMN_WIDTHS['Calibri'][11]['height'] * $size / 11.0; } return $rowHeight; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php index 5c6ccfd3acc..1ec7f6abfcb 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php @@ -415,7 +415,7 @@ class EigenvalueDecomposition $norm = 0.0; for ($i = 0; $i < $nn; ++$i) { - if (($i < $low) || ($i > $high)) { + if ($i > $high) { $this->d[$i] = $this->H[$i][$i]; $this->e[$i] = 0.0; } @@ -495,7 +495,7 @@ class EigenvalueDecomposition $this->V[$i][$n - 1] = $q * $z + $p * $this->V[$i][$n]; $this->V[$i][$n] = $q * $this->V[$i][$n] - $p * $z; } - // Complex pair + // Complex pair } else { $this->d[$n - 1] = $x + $p; $this->d[$n] = $x + $p; @@ -671,7 +671,7 @@ class EigenvalueDecomposition } else { $this->H[$i][$n] = -$r / ($eps * $norm); } - // Solve real equations + // Solve real equations } else { $x = $this->H[$i][$i + 1]; $y = $this->H[$i + 1][$i]; @@ -693,7 +693,7 @@ class EigenvalueDecomposition } } } - // Complex vector + // Complex vector } elseif ($q < 0) { $l = $n - 1; // Last vector component imaginary so matrix is triangular @@ -762,7 +762,7 @@ class EigenvalueDecomposition // Vectors of isolated roots for ($i = 0; $i < $nn; ++$i) { - if ($i < $low | $i > $high) { + if ($i > $high) { for ($j = $i; $j < $nn; ++$j) { $this->V[$i][$j] = $this->H[$i][$j]; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index adf399ac7fb..5e35d491955 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared\JAMA; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; /** @@ -66,21 +67,21 @@ class Matrix $this->A = $args[0]; break; - //Square matrix - n x n + //Square matrix - n x n case 'integer': $this->m = $args[0]; $this->n = $args[0]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n + //Rectangular matrix - m x n case 'integer,integer': $this->m = $args[0]; $this->n = $args[1]; $this->A = array_fill(0, $this->m, array_fill(0, $this->n, 0)); break; - //Rectangular matrix - m x n initialized from packed array + //Rectangular matrix - m x n initialized from packed array case 'array,integer': $this->m = $args[1]; if ($this->m != 0) { @@ -190,7 +191,7 @@ class Matrix return $R; break; - //A($i0...$iF; $j0...$jF) + //A($i0...$iF; $j0...$jF) case 'integer,integer,integer,integer': [$i0, $iF, $j0, $jF] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -213,7 +214,7 @@ class Matrix return $R; break; - //$R = array of row indices; $C = array of column indices + //$R = array of row indices; $C = array of column indices case 'array,array': [$RL, $CL] = $args; if (count($RL) > 0) { @@ -236,7 +237,7 @@ class Matrix return $R; break; - //A($i0...$iF); $CL = array of column indices + //A($i0...$iF); $CL = array of column indices case 'integer,integer,array': [$i0, $iF, $CL] = $args; if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { @@ -259,7 +260,7 @@ class Matrix return $R; break; - //$RL = array of row indices + //$RL = array of row indices case 'array,integer,integer': [$RL, $j0, $jF] = $args; if (count($RL) > 0) { @@ -532,18 +533,12 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] += $value; } else { - $this->A[$i][$j] = Functions::NAN(); + $this->A[$i][$j] = ExcelError::NAN(); } } } @@ -632,18 +627,12 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] -= $value; } else { - $this->A[$i][$j] = Functions::NAN(); + $this->A[$i][$j] = ExcelError::NAN(); } } } @@ -734,18 +723,12 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] *= $value; } else { - $this->A[$i][$j] = Functions::NAN(); + $this->A[$i][$j] = ExcelError::NAN(); } } } @@ -792,14 +775,8 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { if ($value == 0) { // Trap for Divide by Zero error @@ -808,7 +785,7 @@ class Matrix $M->set($i, $j, $this->A[$i][$j] / $value); } } else { - $M->set($i, $j, Functions::NAN()); + $M->set($i, $j, ExcelError::NAN()); } } } @@ -1079,18 +1056,12 @@ class Matrix for ($j = 0; $j < $this->n; ++$j) { $validValues = true; $value = $M->get($i, $j); - if ((is_string($this->A[$i][$j])) && (strlen($this->A[$i][$j]) > 0) && (!is_numeric($this->A[$i][$j]))) { - $this->A[$i][$j] = trim($this->A[$i][$j], '"'); - $validValues &= StringHelper::convertToNumberIfFraction($this->A[$i][$j]); - } - if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { - $value = trim($value, '"'); - $validValues &= StringHelper::convertToNumberIfFraction($value); - } + [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); + [$value, $validValues] = $this->validateExtractedValue($value, $validValues); if ($validValues) { $this->A[$i][$j] = $this->A[$i][$j] ** $value; } else { - $this->A[$i][$j] = Functions::NAN(); + $this->A[$i][$j] = ExcelError::NAN(); } } } @@ -1134,6 +1105,7 @@ class Matrix $this->checkMatrixDimensions($M); for ($i = 0; $i < $this->m; ++$i) { for ($j = 0; $j < $this->n; ++$j) { + // @phpstan-ignore-next-line $this->A[$i][$j] = trim($this->A[$i][$j], '"') . trim($M->get($i, $j), '"'); } } @@ -1186,4 +1158,20 @@ class Matrix return $L->det(); } + + /** + * @param mixed $value + */ + private function validateExtractedValue($value, bool $validValues): array + { + if (!is_numeric($value) && is_array($value)) { + $value = Functions::flattenArray($value)[0]; + } + if ((is_string($value)) && (strlen($value) > 0) && (!is_numeric($value))) { + $value = trim($value, '"'); + $validValues &= StringHelper::convertToNumberIfFraction($value); + } + + return [$value, $validValues]; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php index 6c8999d0289..b809bfa1dd0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php @@ -315,7 +315,7 @@ class SingularValueDecomposition } break; - // Split at negligible s(k). + // Split at negligible s(k). case 2: $f = $e[$k - 1]; $e[$k - 1] = 0.0; @@ -336,7 +336,7 @@ class SingularValueDecomposition } break; - // Perform one qr step. + // Perform one qr step. case 3: // Calculate the shift. $scale = max(max(max(max(abs($this->s[$p - 1]), abs($this->s[$p - 2])), abs($e[$p - 2])), abs($this->s[$k])), abs($e[$k])); @@ -396,7 +396,7 @@ class SingularValueDecomposition $iter = $iter + 1; break; - // Convergence. + // Convergence. case 4: // Make the singular values positive. if ($this->s[$k] <= 0.0) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php index 435d0e4cda5..16026c3ca00 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/StringHelper.php @@ -9,7 +9,7 @@ class StringHelper /** Constants */ /** Regular Expressions */ // Fraction - const STRING_REGEXP_FRACTION = '(-?)(\d+)\s+(\d+\/\d+)'; + const STRING_REGEXP_FRACTION = '~^\s*(-?)((\d*)\s+)?(\d+\/\d+)\s*$~'; /** * Control characters array. @@ -28,14 +28,14 @@ class StringHelper /** * Decimal separator. * - * @var string + * @var ?string */ private static $decimalSeparator; /** * Thousands separator. * - * @var string + * @var ?string */ private static $thousandsSeparator; @@ -49,7 +49,7 @@ class StringHelper /** * Is iconv extension avalable? * - * @var bool + * @var ?bool */ private static $isIconvEnabled; @@ -328,49 +328,48 @@ class StringHelper } /** - * Try to sanitize UTF8, stripping invalid byte sequences. Not perfect. Does not surrogate characters. - * - * @param string $textValue - * - * @return string + * Try to sanitize UTF8, replacing invalid sequences with Unicode substitution characters. */ - public static function sanitizeUTF8($textValue) + public static function sanitizeUTF8(string $textValue): string { - if (self::getIsIconvEnabled()) { - $textValue = @iconv('UTF-8', 'UTF-8', $textValue); + $textValue = str_replace(["\xef\xbf\xbe", "\xef\xbf\xbf"], "\xef\xbf\xbd", $textValue); + $subst = mb_substitute_character(); // default is question mark + mb_substitute_character(65533); // Unicode substitution character + // Phpstan does not think this can return false. + $returnValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); + mb_substitute_character(/** @scrutinizer ignore-type */ $subst); - return $textValue; - } + return self::returnString($returnValue); + } - $textValue = mb_convert_encoding($textValue, 'UTF-8', 'UTF-8'); - - return $textValue; + /** + * Strictly to satisfy Scrutinizer. + * + * @param mixed $value + */ + private static function returnString($value): string + { + return is_string($value) ? $value : ''; } /** * Check if a string contains UTF8 data. - * - * @param string $textValue - * - * @return bool */ - public static function isUTF8($textValue) + public static function isUTF8(string $textValue): bool { - return $textValue === '' || preg_match('/^./su', $textValue) === 1; + return $textValue === self::sanitizeUTF8($textValue); } /** * Formats a numeric value as a string for output in various output writers forcing * point as decimal separator in case locale is other than English. * - * @param mixed $numericValue - * - * @return string + * @param float|int|string $numericValue */ - public static function formatNumber($numericValue) + public static function formatNumber($numericValue): string { if (is_float($numericValue)) { - return str_replace(',', '.', $numericValue); + return str_replace(',', '.', (string) $numericValue); } return (string) $numericValue; @@ -385,10 +384,8 @@ class StringHelper * * @param string $textValue UTF-8 encoded string * @param mixed[] $arrcRuns Details of rich text runs in $value - * - * @return string */ - public static function UTF8toBIFF8UnicodeShort($textValue, $arrcRuns = []) + public static function UTF8toBIFF8UnicodeShort(string $textValue, array $arrcRuns = []): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -419,10 +416,8 @@ class StringHelper * see OpenOffice.org's Documentation of the Microsoft Excel File Format, sect. 2.5.3. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function UTF8toBIFF8UnicodeLong($textValue) + public static function UTF8toBIFF8UnicodeLong(string $textValue): string { // character count $ln = self::countCharacters($textValue, 'UTF-8'); @@ -436,13 +431,10 @@ class StringHelper /** * Convert string from one encoding to another. * - * @param string $textValue * @param string $to Encoding to convert to, e.g. 'UTF-8' * @param string $from Encoding to convert from, e.g. 'UTF-16LE' - * - * @return string */ - public static function convertEncoding($textValue, $to, $from) + public static function convertEncoding(string $textValue, string $to, string $from): string { if (self::getIsIconvEnabled()) { $result = iconv($from, $to . self::$iconvOptions, $textValue); @@ -451,20 +443,19 @@ class StringHelper } } - return mb_convert_encoding($textValue, $to, $from); + return self::returnString(mb_convert_encoding($textValue, $to, $from)); } /** * Get character count. * - * @param string $textValue * @param string $encoding Encoding * * @return int Character count */ - public static function countCharacters($textValue, $encoding = 'UTF-8') + public static function countCharacters(string $textValue, string $encoding = 'UTF-8'): int { - return mb_strlen($textValue ?? '', $encoding); + return mb_strlen($textValue, $encoding); } /** @@ -472,11 +463,9 @@ class StringHelper * * @param string $textValue UTF-8 encoded string * @param int $offset Start offset - * @param int $length Maximum number of characters in substring - * - * @return string + * @param ?int $length Maximum number of characters in substring */ - public static function substring($textValue, $offset, $length = 0) + public static function substring(string $textValue, int $offset, ?int $length = 0): string { return mb_substr($textValue, $offset, $length, 'UTF-8'); } @@ -485,24 +474,20 @@ class StringHelper * Convert a UTF-8 encoded string to upper case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToUpper($textValue) + public static function strToUpper(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_UPPER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_UPPER, 'UTF-8'); } /** * Convert a UTF-8 encoded string to lower case. * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToLower($textValue) + public static function strToLower(string $textValue): string { - return mb_convert_case($textValue ?? '', MB_CASE_LOWER, 'UTF-8'); + return mb_convert_case($textValue, MB_CASE_LOWER, 'UTF-8'); } /** @@ -510,24 +495,27 @@ class StringHelper * (uppercase every first character in each word, lower case all other characters). * * @param string $textValue UTF-8 encoded string - * - * @return string */ - public static function strToTitle($textValue) + public static function strToTitle(string $textValue): string { return mb_convert_case($textValue, MB_CASE_TITLE, 'UTF-8'); } - public static function mbIsUpper($character) + public static function mbIsUpper(string $character): bool { - return mb_strtolower($character, 'UTF-8') != $character; + return mb_strtolower($character, 'UTF-8') !== $character; } - public static function mbStrSplit($string) + /** + * Splits a UTF-8 string into an array of individual characters. + */ + public static function mbStrSplit(string $string): array { // Split at all position not after the start: ^ // and not before the end: $ - return preg_split('/(?_calculateFormulaValue($fractionFormula); return true; @@ -578,10 +563,8 @@ class StringHelper /** * Get the decimal separator. If it has not yet been set explicitly, try to obtain number * formatting information from locale. - * - * @return string */ - public static function getDecimalSeparator() + public static function getDecimalSeparator(): string { if (!isset(self::$decimalSeparator)) { $localeconv = localeconv(); @@ -603,7 +586,7 @@ class StringHelper * * @param string $separator Character for decimal separator */ - public static function setDecimalSeparator($separator): void + public static function setDecimalSeparator(string $separator): void { self::$decimalSeparator = $separator; } @@ -611,10 +594,8 @@ class StringHelper /** * Get the thousands separator. If it has not yet been set explicitly, try to obtain number * formatting information from locale. - * - * @return string */ - public static function getThousandsSeparator() + public static function getThousandsSeparator(): string { if (!isset(self::$thousandsSeparator)) { $localeconv = localeconv(); @@ -636,7 +617,7 @@ class StringHelper * * @param string $separator Character for thousands separator */ - public static function setThousandsSeparator($separator): void + public static function setThousandsSeparator(string $separator): void { self::$thousandsSeparator = $separator; } @@ -644,10 +625,8 @@ class StringHelper /** * Get the currency code. If it has not yet been set explicitly, try to obtain the * symbol information from locale. - * - * @return string */ - public static function getCurrencyCode() + public static function getCurrencyCode(): string { if (!empty(self::$currencyCode)) { return self::$currencyCode; @@ -674,7 +653,7 @@ class StringHelper * * @param string $currencyCode Character for currency code */ - public static function setCurrencyCode($currencyCode): void + public static function setCurrencyCode(string $currencyCode): void { self::$currencyCode = $currencyCode; } @@ -682,11 +661,11 @@ class StringHelper /** * Convert SYLK encoded string to UTF-8. * - * @param string $textValue + * @param string $textValue SYLK encoded string * * @return string UTF-8 encoded string */ - public static function SYLKtoUTF8($textValue) + public static function SYLKtoUTF8(string $textValue): string { self::buildCharacterSets(); @@ -717,6 +696,6 @@ class StringHelper } $v = (float) $textValue; - return (is_numeric(substr($textValue, 0, strlen($v)))) ? $v : $textValue; + return (is_numeric(substr($textValue, 0, strlen((string) $v)))) ? $v : $textValue; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php index dabb88f2d08..734c076d3f9 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/TimeZone.php @@ -21,9 +21,9 @@ class TimeZone * * @return bool Success or failure */ - private static function validateTimeZone($timezoneName) + private static function validateTimeZone(string $timezoneName): bool { - return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC)); + return in_array($timezoneName, DateTimeZone::listIdentifiers(DateTimeZone::ALL_WITH_BC), true); } /** @@ -33,7 +33,7 @@ class TimeZone * * @return bool Success or failure */ - public static function setTimeZone($timezoneName) + public static function setTimeZone(string $timezoneName): bool { if (self::validateTimezone($timezoneName)) { self::$timezone = $timezoneName; @@ -49,7 +49,7 @@ class TimeZone * * @return string Timezone (e.g. 'Europe/London') */ - public static function getTimeZone() + public static function getTimeZone(): string { return self::$timezone; } @@ -63,7 +63,7 @@ class TimeZone * * @return int Number of seconds for timezone adjustment */ - public static function getTimeZoneAdjustment($timezoneName, $timestamp) + public static function getTimeZoneAdjustment(?string $timezoneName, $timestamp): int { $timezoneName = $timezoneName ?? self::$timezone; $dtobj = Date::dateTimeFromTimestamp("$timestamp"); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php index 2c8eea5b7cd..b10a0a10822 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php @@ -114,6 +114,7 @@ class PolynomialBestFit extends BestFit public function getCoefficients($dp = 0) { + // @phpstan-ignore-next-line return array_merge([$this->getIntersect($dp)], $this->getSlope($dp)); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php index 61d1183aba3..929f59b9cf1 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/Trend/Trend.php @@ -72,6 +72,7 @@ class Trend case self::TREND_POWER: if (!isset(self::$trendCache[$key])) { $className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit'; + // @phpstan-ignore-next-line self::$trendCache[$key] = new $className($yValues, $xValues, $const); } @@ -82,7 +83,7 @@ class Trend case self::TREND_POLYNOMIAL_5: case self::TREND_POLYNOMIAL_6: if (!isset(self::$trendCache[$key])) { - $order = substr($trendType, -1); + $order = (int) substr($trendType, -1); self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues); } @@ -100,7 +101,7 @@ class Trend } if ($trendType != self::TREND_BEST_FIT_NO_POLY) { foreach (self::$trendTypePolynomialOrders as $trendMethod) { - $order = substr($trendMethod, -1); + $order = (int) substr($trendMethod, -1); $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues); if ($bestFit[$trendMethod]->getError()) { unset($bestFit[$trendMethod]); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php index 84ad8a83806..3dc0aad97aa 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Shared/XMLWriter.php @@ -54,7 +54,9 @@ class XMLWriter extends \XMLWriter public function __destruct() { // Unlink temporary files + // There is nothing reasonable to do if unlink fails. if ($this->tempFileName != '') { + /** @scrutinizer ignore-unhandled */ @unlink($this->tempFileName); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php index 350ba652fcc..364700e253c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Spreadsheet.php @@ -3,10 +3,13 @@ namespace PhpOffice\PhpSpreadsheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Iterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter; class Spreadsheet { @@ -313,7 +316,7 @@ class Spreadsheet break; case 'target': case 'data': - if (is_array($this->ribbonXMLData) && isset($this->ribbonXMLData[$what])) { + if (is_array($this->ribbonXMLData)) { $returnData = $this->ribbonXMLData[$what]; } @@ -608,7 +611,7 @@ class Spreadsheet /** * Add sheet. * - * @param Worksheet $worksheet The worskeet to add + * @param Worksheet $worksheet The worksheet to add * @param null|int $sheetIndex Index where sheet should go (0,1,..., or null for last) * * @return Worksheet @@ -721,6 +724,19 @@ class Spreadsheet return null; } + /** + * Get sheet by name, throwing exception if not found. + */ + public function getSheetByNameOrThrow(string $worksheetName): Worksheet + { + $worksheet = $this->getSheetByName($worksheetName); + if ($worksheet === null) { + throw new Exception("Sheet $worksheetName does not exist."); + } + + return $worksheet; + } + /** * Get index for sheet. * @@ -869,6 +885,19 @@ class Spreadsheet $cell->setXfIndex($cell->getXfIndex() + $countCellXfs); } + // update the column dimensions Xfs + foreach ($worksheet->getColumnDimensions() as $columnDimension) { + $columnDimension->setXfIndex($columnDimension->getXfIndex() + $countCellXfs); + } + + // update the row dimensions Xfs + foreach ($worksheet->getRowDimensions() as $rowDimension) { + $xfIndex = $rowDimension->getXfIndex(); + if ($xfIndex !== null) { + $rowDimension->setXfIndex($xfIndex + $countCellXfs); + } + } + return $this->addSheet($worksheet, $sheetIndex); } @@ -1107,28 +1136,24 @@ class Spreadsheet */ public function copy() { - $copied = clone $this; + $filename = File::temporaryFilename(); + $writer = new XlsxWriter($this); + $writer->setIncludeCharts(true); + $writer->save($filename); - $worksheetCount = count($this->workSheetCollection); - for ($i = 0; $i < $worksheetCount; ++$i) { - $this->workSheetCollection[$i] = $this->workSheetCollection[$i]->copy(); - $this->workSheetCollection[$i]->rebindParent($this); - } + $reader = new XlsxReader(); + $reader->setIncludeCharts(true); + $reloadedSpreadsheet = $reader->load($filename); + unlink($filename); - return $copied; + return $reloadedSpreadsheet; } - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ public function __clone() { - // @phpstan-ignore-next-line - foreach ($this as $key => $val) { - if (is_object($val) || (is_array($val))) { - $this->{$key} = unserialize(serialize($val)); - } - } + throw new Exception( + 'Do not use clone on spreadsheet. Use spreadsheet->copy() instead.' + ); } /** @@ -1549,7 +1574,7 @@ class Spreadsheet * Workbook window is hidden and cannot be shown in the * user interface. * - * @param string $visibility visibility status of the workbook + * @param null|string $visibility visibility status of the workbook */ public function setVisibility($visibility): void { @@ -1583,7 +1608,7 @@ class Spreadsheet */ public function setTabRatio($tabRatio): void { - if ($tabRatio >= 0 || $tabRatio <= 1000) { + if ($tabRatio >= 0 && $tabRatio <= 1000) { $this->tabRatio = (int) $tabRatio; } else { throw new Exception('Tab ratio must be between 0 and 1000.'); @@ -1602,4 +1627,14 @@ class Spreadsheet } } } + + /** + * Silliness to mollify Scrutinizer. + * + * @codeCoverageIgnore + */ + public function getSharedComponent(): Style + { + return new Style(); + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php index 83ac5b0dabf..68edfaca40c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Alignment.php @@ -15,6 +15,27 @@ class Alignment extends Supervisor const HORIZONTAL_JUSTIFY = 'justify'; const HORIZONTAL_FILL = 'fill'; const HORIZONTAL_DISTRIBUTED = 'distributed'; // Excel2007 only + private const HORIZONTAL_CENTER_CONTINUOUS_LC = 'centercontinuous'; + // Mapping for horizontal alignment + const HORIZONTAL_ALIGNMENT_FOR_XLSX = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER_CONTINUOUS, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_DISTRIBUTED, + ]; + // Mapping for horizontal alignment CSS + const HORIZONTAL_ALIGNMENT_FOR_HTML = [ + self::HORIZONTAL_LEFT => self::HORIZONTAL_LEFT, + self::HORIZONTAL_RIGHT => self::HORIZONTAL_RIGHT, + self::HORIZONTAL_CENTER => self::HORIZONTAL_CENTER, + self::HORIZONTAL_CENTER_CONTINUOUS => self::HORIZONTAL_CENTER, + self::HORIZONTAL_JUSTIFY => self::HORIZONTAL_JUSTIFY, + //self::HORIZONTAL_FILL => self::HORIZONTAL_FILL, // no reasonable equivalent for fill + self::HORIZONTAL_DISTRIBUTED => self::HORIZONTAL_JUSTIFY, + ]; // Vertical alignment styles const VERTICAL_BOTTOM = 'bottom'; @@ -22,6 +43,45 @@ class Alignment extends Supervisor const VERTICAL_CENTER = 'center'; const VERTICAL_JUSTIFY = 'justify'; const VERTICAL_DISTRIBUTED = 'distributed'; // Excel2007 only + // Vertical alignment CSS + private const VERTICAL_BASELINE = 'baseline'; + private const VERTICAL_MIDDLE = 'middle'; + private const VERTICAL_SUB = 'sub'; + private const VERTICAL_SUPER = 'super'; + private const VERTICAL_TEXT_BOTTOM = 'text-bottom'; + private const VERTICAL_TEXT_TOP = 'text-top'; + + // Mapping for vertical alignment + const VERTICAL_ALIGNMENT_FOR_XLSX = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_CENTER, + self::VERTICAL_JUSTIFY => self::VERTICAL_JUSTIFY, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_DISTRIBUTED, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BOTTOM, + self::VERTICAL_MIDDLE => self::VERTICAL_CENTER, + self::VERTICAL_SUB => self::VERTICAL_BOTTOM, + self::VERTICAL_SUPER => self::VERTICAL_TOP, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TOP, + ]; + + // Mapping for vertical alignment for Html + const VERTICAL_ALIGNMENT_FOR_HTML = [ + self::VERTICAL_BOTTOM => self::VERTICAL_BOTTOM, + self::VERTICAL_TOP => self::VERTICAL_TOP, + self::VERTICAL_CENTER => self::VERTICAL_MIDDLE, + self::VERTICAL_JUSTIFY => self::VERTICAL_MIDDLE, + self::VERTICAL_DISTRIBUTED => self::VERTICAL_MIDDLE, + // css settings that arent't in sync with Excel + self::VERTICAL_BASELINE => self::VERTICAL_BASELINE, + self::VERTICAL_MIDDLE => self::VERTICAL_MIDDLE, + self::VERTICAL_SUB => self::VERTICAL_SUB, + self::VERTICAL_SUPER => self::VERTICAL_SUPER, + self::VERTICAL_TEXT_BOTTOM => self::VERTICAL_TEXT_BOTTOM, + self::VERTICAL_TEXT_TOP => self::VERTICAL_TEXT_TOP, + ]; // Read order const READORDER_CONTEXT = 0; @@ -202,8 +262,9 @@ class Alignment extends Supervisor */ public function setHorizontal(string $horizontalAlignment) { - if ($horizontalAlignment == '') { - $horizontalAlignment = self::HORIZONTAL_GENERAL; + $horizontalAlignment = strtolower($horizontalAlignment); + if ($horizontalAlignment === self::HORIZONTAL_CENTER_CONTINUOUS_LC) { + $horizontalAlignment = self::HORIZONTAL_CENTER_CONTINUOUS; } if ($this->isSupervisor) { @@ -239,9 +300,7 @@ class Alignment extends Supervisor */ public function setVertical($verticalAlignment) { - if ($verticalAlignment == '') { - $verticalAlignment = self::VERTICAL_BOTTOM; - } + $verticalAlignment = strtolower($verticalAlignment); if ($this->isSupervisor) { $styleArray = $this->getStyleArray(['vertical' => $verticalAlignment]); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php index c2d4f749055..922be803c10 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Color.php @@ -26,17 +26,83 @@ class Color extends Supervisor const COLOR_DARKGREEN = 'FF008000'; const COLOR_YELLOW = 'FFFFFF00'; const COLOR_DARKYELLOW = 'FF808000'; + const COLOR_MAGENTA = 'FFFF00FF'; + const COLOR_CYAN = 'FF00FFFF'; + + const NAMED_COLOR_TRANSLATIONS = [ + 'Black' => self::COLOR_BLACK, + 'White' => self::COLOR_WHITE, + 'Red' => self::COLOR_RED, + 'Green' => self::COLOR_GREEN, + 'Blue' => self::COLOR_BLUE, + 'Yellow' => self::COLOR_YELLOW, + 'Magenta' => self::COLOR_MAGENTA, + 'Cyan' => self::COLOR_CYAN, + ]; const VALIDATE_ARGB_SIZE = 8; const VALIDATE_RGB_SIZE = 6; - const VALIDATE_COLOR_VALUE = '/^[A-F0-9]{%d}$/i'; + const VALIDATE_COLOR_6 = '/^[A-F0-9]{6}$/i'; + const VALIDATE_COLOR_8 = '/^[A-F0-9]{8}$/i'; - /** - * Indexed colors array. - * - * @var array - */ - protected static $indexedColors; + private const INDEXED_COLORS = [ + 1 => 'FF000000', // System Colour #1 - Black + 2 => 'FFFFFFFF', // System Colour #2 - White + 3 => 'FFFF0000', // System Colour #3 - Red + 4 => 'FF00FF00', // System Colour #4 - Green + 5 => 'FF0000FF', // System Colour #5 - Blue + 6 => 'FFFFFF00', // System Colour #6 - Yellow + 7 => 'FFFF00FF', // System Colour #7- Magenta + 8 => 'FF00FFFF', // System Colour #8- Cyan + 9 => 'FF800000', // Standard Colour #9 + 10 => 'FF008000', // Standard Colour #10 + 11 => 'FF000080', // Standard Colour #11 + 12 => 'FF808000', // Standard Colour #12 + 13 => 'FF800080', // Standard Colour #13 + 14 => 'FF008080', // Standard Colour #14 + 15 => 'FFC0C0C0', // Standard Colour #15 + 16 => 'FF808080', // Standard Colour #16 + 17 => 'FF9999FF', // Chart Fill Colour #17 + 18 => 'FF993366', // Chart Fill Colour #18 + 19 => 'FFFFFFCC', // Chart Fill Colour #19 + 20 => 'FFCCFFFF', // Chart Fill Colour #20 + 21 => 'FF660066', // Chart Fill Colour #21 + 22 => 'FFFF8080', // Chart Fill Colour #22 + 23 => 'FF0066CC', // Chart Fill Colour #23 + 24 => 'FFCCCCFF', // Chart Fill Colour #24 + 25 => 'FF000080', // Chart Line Colour #25 + 26 => 'FFFF00FF', // Chart Line Colour #26 + 27 => 'FFFFFF00', // Chart Line Colour #27 + 28 => 'FF00FFFF', // Chart Line Colour #28 + 29 => 'FF800080', // Chart Line Colour #29 + 30 => 'FF800000', // Chart Line Colour #30 + 31 => 'FF008080', // Chart Line Colour #31 + 32 => 'FF0000FF', // Chart Line Colour #32 + 33 => 'FF00CCFF', // Standard Colour #33 + 34 => 'FFCCFFFF', // Standard Colour #34 + 35 => 'FFCCFFCC', // Standard Colour #35 + 36 => 'FFFFFF99', // Standard Colour #36 + 37 => 'FF99CCFF', // Standard Colour #37 + 38 => 'FFFF99CC', // Standard Colour #38 + 39 => 'FFCC99FF', // Standard Colour #39 + 40 => 'FFFFCC99', // Standard Colour #40 + 41 => 'FF3366FF', // Standard Colour #41 + 42 => 'FF33CCCC', // Standard Colour #42 + 43 => 'FF99CC00', // Standard Colour #43 + 44 => 'FFFFCC00', // Standard Colour #44 + 45 => 'FFFF9900', // Standard Colour #45 + 46 => 'FFFF6600', // Standard Colour #46 + 47 => 'FF666699', // Standard Colour #47 + 48 => 'FF969696', // Standard Colour #48 + 49 => 'FF003366', // Standard Colour #49 + 50 => 'FF339966', // Standard Colour #50 + 51 => 'FF003300', // Standard Colour #51 + 52 => 'FF333300', // Standard Colour #52 + 53 => 'FF993300', // Standard Colour #53 + 54 => 'FF993366', // Standard Colour #54 + 55 => 'FF333399', // Standard Colour #55 + 56 => 'FF333333', // Standard Colour #56 + ]; /** * ARGB - Alpha RGB. @@ -66,7 +132,7 @@ class Color extends Supervisor // Initialise values if (!$isConditional) { - $this->argb = $this->validateColor($colorValue, self::VALIDATE_ARGB_SIZE) ? $colorValue : self::COLOR_BLACK; + $this->argb = $this->validateColor($colorValue) ?: self::COLOR_BLACK; } } @@ -135,10 +201,23 @@ class Color extends Supervisor return $this; } - private function validateColor(string $colorValue, int $size): bool + private function validateColor(?string $colorValue): string { - return in_array(ucfirst(strtolower($colorValue)), self::NAMED_COLORS) || - preg_match(sprintf(self::VALIDATE_COLOR_VALUE, $size), $colorValue); + if ($colorValue === null || $colorValue === '') { + return self::COLOR_BLACK; + } + $named = ucfirst(strtolower($colorValue)); + if (array_key_exists($named, self::NAMED_COLOR_TRANSLATIONS)) { + return self::NAMED_COLOR_TRANSLATIONS[$named]; + } + if (preg_match(self::VALIDATE_COLOR_8, $colorValue) === 1) { + return $colorValue; + } + if (preg_match(self::VALIDATE_COLOR_6, $colorValue) === 1) { + return 'FF' . $colorValue; + } + + return ''; } /** @@ -163,9 +242,8 @@ class Color extends Supervisor public function setARGB(?string $colorValue = self::COLOR_BLACK) { $this->hasChanged = true; - if ($colorValue === '' || $colorValue === null) { - $colorValue = self::COLOR_BLACK; - } elseif (!$this->validateColor($colorValue, self::VALIDATE_ARGB_SIZE)) { + $colorValue = $this->validateColor($colorValue); + if ($colorValue === '') { return $this; } @@ -200,21 +278,7 @@ class Color extends Supervisor */ public function setRGB(?string $colorValue = self::COLOR_BLACK) { - $this->hasChanged = true; - if ($colorValue === '' || $colorValue === null) { - $colorValue = '000000'; - } elseif (!$this->validateColor($colorValue, self::VALIDATE_RGB_SIZE)) { - return $this; - } - - if ($this->isSupervisor) { - $styleArray = $this->getStyleArray(['argb' => 'FF' . $colorValue]); - $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); - } else { - $this->argb = 'FF' . $colorValue; - } - - return $this; + return $this->setARGB($colorValue); } /** @@ -229,7 +293,10 @@ class Color extends Supervisor */ private static function getColourComponent($rgbValue, $offset, $hex = true) { - $colour = substr($rgbValue, $offset, 2); + $colour = substr($rgbValue, $offset, 2) ?: ''; + if (preg_match('/^[0-9a-f]{2}$/i', $colour) !== 1) { + $colour = '00'; + } return ($hex) ? $colour : (int) hexdec($colour); } @@ -320,78 +387,20 @@ class Color extends Supervisor * @param int $colorIndex Index entry point into the colour array * @param bool $background Flag to indicate whether default background or foreground colour * should be returned if the indexed colour doesn't exist - * - * @return Color */ - public static function indexedColor($colorIndex, $background = false): self + public static function indexedColor($colorIndex, $background = false, ?array $palette = null): self { // Clean parameter $colorIndex = (int) $colorIndex; - // Indexed colors - if (self::$indexedColors === null) { - self::$indexedColors = [ - 1 => 'FF000000', // System Colour #1 - Black - 2 => 'FFFFFFFF', // System Colour #2 - White - 3 => 'FFFF0000', // System Colour #3 - Red - 4 => 'FF00FF00', // System Colour #4 - Green - 5 => 'FF0000FF', // System Colour #5 - Blue - 6 => 'FFFFFF00', // System Colour #6 - Yellow - 7 => 'FFFF00FF', // System Colour #7- Magenta - 8 => 'FF00FFFF', // System Colour #8- Cyan - 9 => 'FF800000', // Standard Colour #9 - 10 => 'FF008000', // Standard Colour #10 - 11 => 'FF000080', // Standard Colour #11 - 12 => 'FF808000', // Standard Colour #12 - 13 => 'FF800080', // Standard Colour #13 - 14 => 'FF008080', // Standard Colour #14 - 15 => 'FFC0C0C0', // Standard Colour #15 - 16 => 'FF808080', // Standard Colour #16 - 17 => 'FF9999FF', // Chart Fill Colour #17 - 18 => 'FF993366', // Chart Fill Colour #18 - 19 => 'FFFFFFCC', // Chart Fill Colour #19 - 20 => 'FFCCFFFF', // Chart Fill Colour #20 - 21 => 'FF660066', // Chart Fill Colour #21 - 22 => 'FFFF8080', // Chart Fill Colour #22 - 23 => 'FF0066CC', // Chart Fill Colour #23 - 24 => 'FFCCCCFF', // Chart Fill Colour #24 - 25 => 'FF000080', // Chart Line Colour #25 - 26 => 'FFFF00FF', // Chart Line Colour #26 - 27 => 'FFFFFF00', // Chart Line Colour #27 - 28 => 'FF00FFFF', // Chart Line Colour #28 - 29 => 'FF800080', // Chart Line Colour #29 - 30 => 'FF800000', // Chart Line Colour #30 - 31 => 'FF008080', // Chart Line Colour #31 - 32 => 'FF0000FF', // Chart Line Colour #32 - 33 => 'FF00CCFF', // Standard Colour #33 - 34 => 'FFCCFFFF', // Standard Colour #34 - 35 => 'FFCCFFCC', // Standard Colour #35 - 36 => 'FFFFFF99', // Standard Colour #36 - 37 => 'FF99CCFF', // Standard Colour #37 - 38 => 'FFFF99CC', // Standard Colour #38 - 39 => 'FFCC99FF', // Standard Colour #39 - 40 => 'FFFFCC99', // Standard Colour #40 - 41 => 'FF3366FF', // Standard Colour #41 - 42 => 'FF33CCCC', // Standard Colour #42 - 43 => 'FF99CC00', // Standard Colour #43 - 44 => 'FFFFCC00', // Standard Colour #44 - 45 => 'FFFF9900', // Standard Colour #45 - 46 => 'FFFF6600', // Standard Colour #46 - 47 => 'FF666699', // Standard Colour #47 - 48 => 'FF969696', // Standard Colour #48 - 49 => 'FF003366', // Standard Colour #49 - 50 => 'FF339966', // Standard Colour #50 - 51 => 'FF003300', // Standard Colour #51 - 52 => 'FF333300', // Standard Colour #52 - 53 => 'FF993300', // Standard Colour #53 - 54 => 'FF993366', // Standard Colour #54 - 55 => 'FF333399', // Standard Colour #55 - 56 => 'FF333333', // Standard Colour #56 - ]; - } - - if (isset(self::$indexedColors[$colorIndex])) { - return new self(self::$indexedColors[$colorIndex]); + if (empty($palette)) { + if (isset(self::INDEXED_COLORS[$colorIndex])) { + return new self(self::INDEXED_COLORS[$colorIndex]); + } + } else { + if (isset($palette[$colorIndex])) { + return new self($palette[$colorIndex]); + } } return ($background) ? new self(self::COLOR_WHITE) : new self(self::COLOR_BLACK); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Conditional.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Conditional.php index e148ee82ba6..019c0648d63 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Conditional.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Conditional.php @@ -9,23 +9,37 @@ class Conditional implements IComparable { // Condition types const CONDITION_NONE = 'none'; + const CONDITION_BEGINSWITH = 'beginsWith'; const CONDITION_CELLIS = 'cellIs'; - const CONDITION_CONTAINSTEXT = 'containsText'; - const CONDITION_EXPRESSION = 'expression'; const CONDITION_CONTAINSBLANKS = 'containsBlanks'; - const CONDITION_NOTCONTAINSBLANKS = 'notContainsBlanks'; + const CONDITION_CONTAINSERRORS = 'containsErrors'; + const CONDITION_CONTAINSTEXT = 'containsText'; const CONDITION_DATABAR = 'dataBar'; + const CONDITION_ENDSWITH = 'endsWith'; + const CONDITION_EXPRESSION = 'expression'; + const CONDITION_NOTCONTAINSBLANKS = 'notContainsBlanks'; + const CONDITION_NOTCONTAINSERRORS = 'notContainsErrors'; const CONDITION_NOTCONTAINSTEXT = 'notContainsText'; + const CONDITION_TIMEPERIOD = 'timePeriod'; + const CONDITION_DUPLICATES = 'duplicateValues'; + const CONDITION_UNIQUE = 'uniqueValues'; private const CONDITION_TYPES = [ + self::CONDITION_BEGINSWITH, self::CONDITION_CELLIS, self::CONDITION_CONTAINSBLANKS, + self::CONDITION_CONTAINSERRORS, self::CONDITION_CONTAINSTEXT, self::CONDITION_DATABAR, + self::CONDITION_DUPLICATES, + self::CONDITION_ENDSWITH, self::CONDITION_EXPRESSION, self::CONDITION_NONE, self::CONDITION_NOTCONTAINSBLANKS, + self::CONDITION_NOTCONTAINSERRORS, self::CONDITION_NOTCONTAINSTEXT, + self::CONDITION_TIMEPERIOD, + self::CONDITION_UNIQUE, ]; // Operator types @@ -43,6 +57,17 @@ class Conditional implements IComparable const OPERATOR_BETWEEN = 'between'; const OPERATOR_NOTBETWEEN = 'notBetween'; + const TIMEPERIOD_TODAY = 'today'; + const TIMEPERIOD_YESTERDAY = 'yesterday'; + const TIMEPERIOD_TOMORROW = 'tomorrow'; + const TIMEPERIOD_LAST_7_DAYS = 'last7Days'; + const TIMEPERIOD_LAST_WEEK = 'lastWeek'; + const TIMEPERIOD_THIS_WEEK = 'thisWeek'; + const TIMEPERIOD_NEXT_WEEK = 'nextWeek'; + const TIMEPERIOD_LAST_MONTH = 'lastMonth'; + const TIMEPERIOD_THIS_MONTH = 'thisMonth'; + const TIMEPERIOD_NEXT_MONTH = 'nextMonth'; + /** * Condition type. * @@ -74,7 +99,7 @@ class Conditional implements IComparable /** * Condition. * - * @var string[] + * @var (bool|float|int|string)[] */ private $condition = []; @@ -198,7 +223,7 @@ class Conditional implements IComparable /** * Get Conditions. * - * @return string[] + * @return (bool|float|int|string)[] */ public function getConditions() { @@ -208,7 +233,7 @@ class Conditional implements IComparable /** * Set Conditions. * - * @param bool|float|int|string|string[] $conditions Condition + * @param bool|float|int|string|(bool|float|int|string)[] $conditions Condition * * @return $this */ @@ -225,7 +250,7 @@ class Conditional implements IComparable /** * Add Condition. * - * @param string $condition Condition + * @param bool|float|int|string $condition Condition * * @return $this */ @@ -251,7 +276,7 @@ class Conditional implements IComparable * * @return $this */ - public function setStyle(?Style $style = null) + public function setStyle(Style $style) { $this->style = $style; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php new file mode 100644 index 00000000000..e0dc0efb900 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellMatcher.php @@ -0,0 +1,312 @@ + '=', + Conditional::OPERATOR_GREATERTHAN => '>', + Conditional::OPERATOR_GREATERTHANOREQUAL => '>=', + Conditional::OPERATOR_LESSTHAN => '<', + Conditional::OPERATOR_LESSTHANOREQUAL => '<=', + Conditional::OPERATOR_NOTEQUAL => '<>', + ]; + + public const COMPARISON_RANGE_OPERATORS = [ + Conditional::OPERATOR_BETWEEN => 'IF(AND(A1>=%s,A1<=%s),TRUE,FALSE)', + Conditional::OPERATOR_NOTBETWEEN => 'IF(AND(A1>=%s,A1<=%s),FALSE,TRUE)', + ]; + + public const COMPARISON_DUPLICATES_OPERATORS = [ + Conditional::CONDITION_DUPLICATES => "COUNTIF('%s'!%s,%s)>1", + Conditional::CONDITION_UNIQUE => "COUNTIF('%s'!%s,%s)=1", + ]; + + /** + * @var Cell + */ + protected $cell; + + /** + * @var int + */ + protected $cellRow; + + /** + * @var Worksheet + */ + protected $worksheet; + + /** + * @var int + */ + protected $cellColumn; + + /** + * @var string + */ + protected $conditionalRange; + + /** + * @var string + */ + protected $referenceCell; + + /** + * @var int + */ + protected $referenceRow; + + /** + * @var int + */ + protected $referenceColumn; + + /** + * @var Calculation + */ + protected $engine; + + public function __construct(Cell $cell, string $conditionalRange) + { + $this->cell = $cell; + $this->worksheet = $cell->getWorksheet(); + [$this->cellColumn, $this->cellRow] = Coordinate::indexesFromString($this->cell->getCoordinate()); + $this->setReferenceCellForExpressions($conditionalRange); + + $this->engine = Calculation::getInstance($this->worksheet->getParent()); + } + + protected function setReferenceCellForExpressions(string $conditionalRange): void + { + $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange))); + [$this->referenceCell] = $conditionalRange[0]; + + [$this->referenceColumn, $this->referenceRow] = Coordinate::indexesFromString($this->referenceCell); + + // Convert our conditional range to an absolute conditional range, so it can be used "pinned" in formulae + $rangeSets = []; + foreach ($conditionalRange as $rangeSet) { + $absoluteRangeSet = array_map( + [Coordinate::class, 'absoluteCoordinate'], + $rangeSet + ); + $rangeSets[] = implode(':', $absoluteRangeSet); + } + $this->conditionalRange = implode(',', $rangeSets); + } + + public function evaluateConditional(Conditional $conditional): bool + { + // Some calculations may modify the stored cell; so reset it before every evaluation. + $cellColumn = Coordinate::stringFromColumnIndex($this->cellColumn); + $cellAddress = "{$cellColumn}{$this->cellRow}"; + $this->cell = $this->worksheet->getCell($cellAddress); + + switch ($conditional->getConditionType()) { + case Conditional::CONDITION_CELLIS: + return $this->processOperatorComparison($conditional); + case Conditional::CONDITION_DUPLICATES: + case Conditional::CONDITION_UNIQUE: + return $this->processDuplicatesComparison($conditional); + case Conditional::CONDITION_CONTAINSTEXT: + // Expression is NOT(ISERROR(SEARCH("",))) + case Conditional::CONDITION_NOTCONTAINSTEXT: + // Expression is ISERROR(SEARCH("",)) + case Conditional::CONDITION_BEGINSWITH: + // Expression is LEFT(,LEN(""))="" + case Conditional::CONDITION_ENDSWITH: + // Expression is RIGHT(,LEN(""))="" + case Conditional::CONDITION_CONTAINSBLANKS: + // Expression is LEN(TRIM())=0 + case Conditional::CONDITION_NOTCONTAINSBLANKS: + // Expression is LEN(TRIM())>0 + case Conditional::CONDITION_CONTAINSERRORS: + // Expression is ISERROR() + case Conditional::CONDITION_NOTCONTAINSERRORS: + // Expression is NOT(ISERROR()) + case Conditional::CONDITION_TIMEPERIOD: + // Expression varies, depending on specified timePeriod value, e.g. + // Yesterday FLOOR(,1)=TODAY()-1 + // Today FLOOR(,1)=TODAY() + // Tomorrow FLOOR(,1)=TODAY()+1 + // Last 7 Days AND(TODAY()-FLOOR(,1)<=6,FLOOR(,1)<=TODAY()) + case Conditional::CONDITION_EXPRESSION: + return $this->processExpression($conditional); + } + + return false; + } + + /** + * @param mixed $value + * + * @return float|int|string + */ + protected function wrapValue($value) + { + if (!is_numeric($value)) { + if (is_bool($value)) { + return $value ? 'TRUE' : 'FALSE'; + } elseif ($value === null) { + return 'NULL'; + } + + return '"' . $value . '"'; + } + + return $value; + } + + /** + * @return float|int|string + */ + protected function wrapCellValue() + { + return $this->wrapValue($this->cell->getCalculatedValue()); + } + + /** + * @return float|int|string + */ + protected function conditionCellAdjustment(array $matches) + { + $column = $matches[6]; + $row = $matches[7]; + + if (strpos($column, '$') === false) { + $column = Coordinate::columnIndexFromString($column); + $column += $this->cellColumn - $this->referenceColumn; + $column = Coordinate::stringFromColumnIndex($column); + } + + if (strpos($row, '$') === false) { + $row += $this->cellRow - $this->referenceRow; + } + + if (!empty($matches[4])) { + $worksheet = $this->worksheet->getParent()->getSheetByName(trim($matches[4], "'")); + if ($worksheet === null) { + return $this->wrapValue(null); + } + + return $this->wrapValue( + $worksheet + ->getCell(str_replace('$', '', "{$column}{$row}")) + ->getCalculatedValue() + ); + } + + return $this->wrapValue( + $this->worksheet + ->getCell(str_replace('$', '', "{$column}{$row}")) + ->getCalculatedValue() + ); + } + + protected function cellConditionCheck(string $condition): string + { + $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition); + $i = false; + foreach ($splitCondition as &$value) { + // Only count/replace in alternating array entries (ie. not in quoted strings) + if ($i = !$i) { + $value = (string) preg_replace_callback( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', + [$this, 'conditionCellAdjustment'], + $value + ); + } + } + unset($value); + // Then rebuild the condition string to return it + return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition); + } + + protected function adjustConditionsForCellReferences(array $conditions): array + { + return array_map( + [$this, 'cellConditionCheck'], + $conditions + ); + } + + protected function processOperatorComparison(Conditional $conditional): bool + { + if (array_key_exists($conditional->getOperatorType(), self::COMPARISON_RANGE_OPERATORS)) { + return $this->processRangeOperator($conditional); + } + + $operator = self::COMPARISON_OPERATORS[$conditional->getOperatorType()]; + $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); + $expression = sprintf('%s%s%s', (string) $this->wrapCellValue(), $operator, (string) array_pop($conditions)); + + return $this->evaluateExpression($expression); + } + + protected function processRangeOperator(Conditional $conditional): bool + { + $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); + sort($conditions); + $expression = sprintf( + (string) preg_replace( + '/\bA1\b/i', + (string) $this->wrapCellValue(), + self::COMPARISON_RANGE_OPERATORS[$conditional->getOperatorType()] + ), + ...$conditions + ); + + return $this->evaluateExpression($expression); + } + + protected function processDuplicatesComparison(Conditional $conditional): bool + { + $worksheetName = $this->cell->getWorksheet()->getTitle(); + + $expression = sprintf( + self::COMPARISON_DUPLICATES_OPERATORS[$conditional->getConditionType()], + $worksheetName, + $this->conditionalRange, + $this->cellConditionCheck($this->cell->getCalculatedValue()) + ); + + return $this->evaluateExpression($expression); + } + + protected function processExpression(Conditional $conditional): bool + { + $conditions = $this->adjustConditionsForCellReferences($conditional->getConditions()); + $expression = array_pop($conditions); + + $expression = (string) preg_replace( + '/\b' . $this->referenceCell . '\b/i', + (string) $this->wrapCellValue(), + $expression + ); + + return $this->evaluateExpression($expression); + } + + protected function evaluateExpression(string $expression): bool + { + $expression = "={$expression}"; + + try { + $this->engine->flushInstance(); + $result = (bool) $this->engine->calculateFormula($expression); + } catch (Exception $e) { + return false; + } + + return $result; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php new file mode 100644 index 00000000000..4c000db32a0 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/CellStyleAssessor.php @@ -0,0 +1,45 @@ +cellMatcher = new CellMatcher($cell, $conditionalRange); + $this->styleMerger = new StyleMerger($cell->getStyle()); + } + + /** + * @param Conditional[] $conditionalStyles + */ + public function matchConditions(array $conditionalStyles = []): Style + { + foreach ($conditionalStyles as $conditional) { + /** @var Conditional $conditional */ + if ($this->cellMatcher->evaluateConditional($conditional) === true) { + // Merging the conditional style into the base style goes in here + $this->styleMerger->mergeStyle($conditional->getStyle()); + if ($conditional->getStopIfTrue() === true) { + break; + } + } + } + + return $this->styleMerger->getStyle(); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php index 54513670e43..f7a2eee1954 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalDataBar.php @@ -11,10 +11,10 @@ class ConditionalDataBar /** children */ - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $minimumConditionalFormatValueObject; - /** @var ConditionalFormatValueObject */ + /** @var ?ConditionalFormatValueObject */ private $maximumConditionalFormatValueObject; /** @var string */ @@ -22,7 +22,7 @@ class ConditionalDataBar /** */ - /** @var ConditionalFormattingRuleExtension */ + /** @var ?ConditionalFormattingRuleExtension */ private $conditionalFormattingRuleExt; /** @@ -43,10 +43,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMinimumConditionalFormatValueObject() + public function getMinimumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->minimumConditionalFormatValueObject; } @@ -58,10 +55,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormatValueObject - */ - public function getMaximumConditionalFormatValueObject() + public function getMaximumConditionalFormatValueObject(): ?ConditionalFormatValueObject { return $this->maximumConditionalFormatValueObject; } @@ -85,10 +79,7 @@ class ConditionalDataBar return $this; } - /** - * @return ConditionalFormattingRuleExtension - */ - public function getConditionalFormattingRuleExt() + public function getConditionalFormattingRuleExt(): ?ConditionalFormattingRuleExtension { return $this->conditionalFormattingRuleExt; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/StyleMerger.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/StyleMerger.php new file mode 100644 index 00000000000..95e6dfd8bdf --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/StyleMerger.php @@ -0,0 +1,118 @@ +baseStyle = $baseStyle; + } + + public function getStyle(): Style + { + return $this->baseStyle; + } + + public function mergeStyle(Style $style): void + { + if ($style->getNumberFormat() !== null && $style->getNumberFormat()->getFormatCode() !== null) { + $this->baseStyle->getNumberFormat()->setFormatCode($style->getNumberFormat()->getFormatCode()); + } + + if ($style->getFont() !== null) { + $this->mergeFontStyle($this->baseStyle->getFont(), $style->getFont()); + } + + if ($style->getFill() !== null) { + $this->mergeFillStyle($this->baseStyle->getFill(), $style->getFill()); + } + + if ($style->getBorders() !== null) { + $this->mergeBordersStyle($this->baseStyle->getBorders(), $style->getBorders()); + } + } + + protected function mergeFontStyle(Font $baseFontStyle, Font $fontStyle): void + { + if ($fontStyle->getBold() !== null) { + $baseFontStyle->setBold($fontStyle->getBold()); + } + + if ($fontStyle->getItalic() !== null) { + $baseFontStyle->setItalic($fontStyle->getItalic()); + } + + if ($fontStyle->getStrikethrough() !== null) { + $baseFontStyle->setStrikethrough($fontStyle->getStrikethrough()); + } + + if ($fontStyle->getUnderline() !== null) { + $baseFontStyle->setUnderline($fontStyle->getUnderline()); + } + + if ($fontStyle->getColor() !== null && $fontStyle->getColor()->getARGB() !== null) { + $baseFontStyle->setColor($fontStyle->getColor()); + } + } + + protected function mergeFillStyle(Fill $baseFillStyle, Fill $fillStyle): void + { + if ($fillStyle->getFillType() !== null) { + $baseFillStyle->setFillType($fillStyle->getFillType()); + } + + if ($fillStyle->getRotation() !== null) { + $baseFillStyle->setRotation($fillStyle->getRotation()); + } + + if ($fillStyle->getStartColor() !== null && $fillStyle->getStartColor()->getARGB() !== null) { + $baseFillStyle->setStartColor($fillStyle->getStartColor()); + } + + if ($fillStyle->getEndColor() !== null && $fillStyle->getEndColor()->getARGB() !== null) { + $baseFillStyle->setEndColor($fillStyle->getEndColor()); + } + } + + protected function mergeBordersStyle(Borders $baseBordersStyle, Borders $bordersStyle): void + { + if ($bordersStyle->getTop() !== null) { + $this->mergeBorderStyle($baseBordersStyle->getTop(), $bordersStyle->getTop()); + } + + if ($bordersStyle->getBottom() !== null) { + $this->mergeBorderStyle($baseBordersStyle->getBottom(), $bordersStyle->getBottom()); + } + + if ($bordersStyle->getLeft() !== null) { + $this->mergeBorderStyle($baseBordersStyle->getLeft(), $bordersStyle->getLeft()); + } + + if ($bordersStyle->getRight() !== null) { + $this->mergeBorderStyle($baseBordersStyle->getRight(), $bordersStyle->getRight()); + } + } + + protected function mergeBorderStyle(Border $baseBorderStyle, Border $borderStyle): void + { + if ($borderStyle->getBorderStyle() !== null) { + $baseBorderStyle->setBorderStyle($borderStyle->getBorderStyle()); + } + + if ($borderStyle->getColor() !== null && $borderStyle->getColor()->getARGB() !== null) { + $baseBorderStyle->setColor($borderStyle->getColor()); + } + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php new file mode 100644 index 00000000000..d5d56a9a3d6 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard.php @@ -0,0 +1,95 @@ +cellRange = $cellRange; + } + + public function newRule(string $ruleType): WizardInterface + { + switch ($ruleType) { + case self::CELL_VALUE: + return new Wizard\CellValue($this->cellRange); + case self::TEXT_VALUE: + return new Wizard\TextValue($this->cellRange); + case self::BLANKS: + return new Wizard\Blanks($this->cellRange, true); + case self::NOT_BLANKS: + return new Wizard\Blanks($this->cellRange, false); + case self::ERRORS: + return new Wizard\Errors($this->cellRange, true); + case self::NOT_ERRORS: + return new Wizard\Errors($this->cellRange, false); + case self::EXPRESSION: + case self::FORMULA: + return new Wizard\Expression($this->cellRange); + case self::DATES_OCCURRING: + return new Wizard\DateValue($this->cellRange); + case self::DUPLICATES: + return new Wizard\Duplicates($this->cellRange, false); + case self::UNIQUE: + return new Wizard\Duplicates($this->cellRange, true); + default: + throw new Exception('No wizard exists for this CF rule type'); + } + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + $conditionalType = $conditional->getConditionType(); + + switch ($conditionalType) { + case Conditional::CONDITION_CELLIS: + return Wizard\CellValue::fromConditional($conditional, $cellRange); + case Conditional::CONDITION_CONTAINSTEXT: + case Conditional::CONDITION_NOTCONTAINSTEXT: + case Conditional::CONDITION_BEGINSWITH: + case Conditional::CONDITION_ENDSWITH: + return Wizard\TextValue::fromConditional($conditional, $cellRange); + case Conditional::CONDITION_CONTAINSBLANKS: + case Conditional::CONDITION_NOTCONTAINSBLANKS: + return Wizard\Blanks::fromConditional($conditional, $cellRange); + case Conditional::CONDITION_CONTAINSERRORS: + case Conditional::CONDITION_NOTCONTAINSERRORS: + return Wizard\Errors::fromConditional($conditional, $cellRange); + case Conditional::CONDITION_TIMEPERIOD: + return Wizard\DateValue::fromConditional($conditional, $cellRange); + case Conditional::CONDITION_EXPRESSION: + return Wizard\Expression::fromConditional($conditional, $cellRange); + case Conditional::CONDITION_DUPLICATES: + case Conditional::CONDITION_UNIQUE: + return Wizard\Duplicates::fromConditional($conditional, $cellRange); + default: + throw new Exception('No wizard exists for this CF rule type'); + } + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php new file mode 100644 index 00000000000..14f30d39e1f --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Blanks.php @@ -0,0 +1,99 @@ + false, + 'isBlank' => true, + 'notEmpty' => false, + 'empty' => true, + ]; + + protected const EXPRESSIONS = [ + Wizard::NOT_BLANKS => 'LEN(TRIM(%s))>0', + Wizard::BLANKS => 'LEN(TRIM(%s))=0', + ]; + + /** + * @var bool + */ + protected $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + protected function getExpression(): void + { + $this->expression = sprintf( + self::EXPRESSIONS[$this->inverse ? Wizard::BLANKS : Wizard::NOT_BLANKS], + $this->referenceCell + ); + } + + public function getConditional(): Conditional + { + $this->getExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_CONTAINSBLANKS : Conditional::CONDITION_NOTCONTAINSBLANKS + ); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_CONTAINSBLANKS && + $conditional->getConditionType() !== Conditional::CONDITION_NOTCONTAINSBLANKS + ) { + throw new Exception('Conditional is not a Blanks CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_CONTAINSBLANKS; + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Blanks CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php new file mode 100644 index 00000000000..f36033dd1cc --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/CellValue.php @@ -0,0 +1,189 @@ + Conditional::OPERATOR_EQUAL, + 'notEquals' => Conditional::OPERATOR_NOTEQUAL, + 'greaterThan' => Conditional::OPERATOR_GREATERTHAN, + 'greaterThanOrEqual' => Conditional::OPERATOR_GREATERTHANOREQUAL, + 'lessThan' => Conditional::OPERATOR_LESSTHAN, + 'lessThanOrEqual' => Conditional::OPERATOR_LESSTHANOREQUAL, + 'between' => Conditional::OPERATOR_BETWEEN, + 'notBetween' => Conditional::OPERATOR_NOTBETWEEN, + ]; + + protected const SINGLE_OPERATORS = CellMatcher::COMPARISON_OPERATORS; + + protected const RANGE_OPERATORS = CellMatcher::COMPARISON_RANGE_OPERATORS; + + /** @var string */ + protected $operator = Conditional::OPERATOR_EQUAL; + + /** @var array */ + protected $operand = [0]; + + /** + * @var string[] + */ + protected $operandValueType = []; + + public function __construct(string $cellRange) + { + parent::__construct($cellRange); + } + + protected function operator(string $operator): void + { + if ((!isset(self::SINGLE_OPERATORS[$operator])) && (!isset(self::RANGE_OPERATORS[$operator]))) { + throw new Exception('Invalid Operator for Cell Value CF Rule Wizard'); + } + + $this->operator = $operator; + } + + /** + * @param mixed $operand + */ + protected function operand(int $index, $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void + { + if (is_string($operand)) { + $operand = $this->validateOperand($operand, $operandValueType); + } + + $this->operand[$index] = $operand; + $this->operandValueType[$index] = $operandValueType; + } + + /** + * @param mixed $value + * + * @return float|int|string + */ + protected function wrapValue($value, string $operandValueType) + { + if (!is_numeric($value) && !is_bool($value) && null !== $value) { + if ($operandValueType === Wizard::VALUE_TYPE_LITERAL) { + return '"' . str_replace('"', '""', $value) . '"'; + } + + return $this->cellConditionCheck($value); + } + + if (null === $value) { + $value = 'NULL'; + } elseif (is_bool($value)) { + $value = $value ? 'TRUE' : 'FALSE'; + } + + return $value; + } + + public function getConditional(): Conditional + { + if (!isset(self::RANGE_OPERATORS[$this->operator])) { + unset($this->operand[1], $this->operandValueType[1]); + } + $values = array_map([$this, 'wrapValue'], $this->operand, $this->operandValueType); + + $conditional = new Conditional(); + $conditional->setConditionType(Conditional::CONDITION_CELLIS); + $conditional->setOperatorType($this->operator); + $conditional->setConditions($values); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + protected static function unwrapString(string $condition): string + { + if ((strpos($condition, '"') === 0) && (strpos(strrev($condition), '"') === 0)) { + $condition = substr($condition, 1, -1); + } + + return str_replace('""', '"', $condition); + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ($conditional->getConditionType() !== Conditional::CONDITION_CELLIS) { + throw new Exception('Conditional is not a Cell Value CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + + $wizard->operator = $conditional->getOperatorType(); + $conditions = $conditional->getConditions(); + foreach ($conditions as $index => $condition) { + // Best-guess to try and identify if the text is a string literal, a cell reference or a formula? + $operandValueType = Wizard::VALUE_TYPE_LITERAL; + if (is_string($condition)) { + if (array_key_exists($condition, Calculation::$excelConstants)) { + $condition = Calculation::$excelConstants[$condition]; + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '$/i', $condition)) { + $operandValueType = Wizard::VALUE_TYPE_CELL; + $condition = self::reverseAdjustCellRef($condition, $cellRange); + } elseif ( + preg_match('/\(\)/', $condition) || + preg_match('/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', $condition) + ) { + $operandValueType = Wizard::VALUE_TYPE_FORMULA; + $condition = self::reverseAdjustCellRef($condition, $cellRange); + } else { + $condition = self::unwrapString($condition); + } + } + $wizard->operand($index, $condition, $operandValueType); + } + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!isset(self::MAGIC_OPERATIONS[$methodName]) && $methodName !== 'and') { + throw new Exception('Invalid Operator for Cell Value CF Rule Wizard'); + } + + if ($methodName === 'and') { + if (!isset(self::RANGE_OPERATORS[$this->operator])) { + throw new Exception('AND Value is only appropriate for range operators'); + } + + $this->operand(1, ...$arguments); + + return $this; + } + + $this->operator(self::MAGIC_OPERATIONS[$methodName]); + $this->operand(0, ...$arguments); + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php new file mode 100644 index 00000000000..453dcdf91b8 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/DateValue.php @@ -0,0 +1,111 @@ + Conditional::TIMEPERIOD_YESTERDAY, + 'today' => Conditional::TIMEPERIOD_TODAY, + 'tomorrow' => Conditional::TIMEPERIOD_TOMORROW, + 'lastSevenDays' => Conditional::TIMEPERIOD_LAST_7_DAYS, + 'last7Days' => Conditional::TIMEPERIOD_LAST_7_DAYS, + 'lastWeek' => Conditional::TIMEPERIOD_LAST_WEEK, + 'thisWeek' => Conditional::TIMEPERIOD_THIS_WEEK, + 'nextWeek' => Conditional::TIMEPERIOD_NEXT_WEEK, + 'lastMonth' => Conditional::TIMEPERIOD_LAST_MONTH, + 'thisMonth' => Conditional::TIMEPERIOD_THIS_MONTH, + 'nextMonth' => Conditional::TIMEPERIOD_NEXT_MONTH, + ]; + + protected const EXPRESSIONS = [ + Conditional::TIMEPERIOD_YESTERDAY => 'FLOOR(%s,1)=TODAY()-1', + Conditional::TIMEPERIOD_TODAY => 'FLOOR(%s,1)=TODAY()', + Conditional::TIMEPERIOD_TOMORROW => 'FLOOR(%s,1)=TODAY()+1', + Conditional::TIMEPERIOD_LAST_7_DAYS => 'AND(TODAY()-FLOOR(%s,1)<=6,FLOOR(%s,1)<=TODAY())', + Conditional::TIMEPERIOD_LAST_WEEK => 'AND(TODAY()-ROUNDDOWN(%s,0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(%s,0)<(WEEKDAY(TODAY())+7))', + Conditional::TIMEPERIOD_THIS_WEEK => 'AND(TODAY()-ROUNDDOWN(%s,0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(%s,0)-TODAY()<=7-WEEKDAY(TODAY()))', + Conditional::TIMEPERIOD_NEXT_WEEK => 'AND(ROUNDDOWN(%s,0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(%s,0)-TODAY()<(15-WEEKDAY(TODAY())))', + Conditional::TIMEPERIOD_LAST_MONTH => 'AND(MONTH(%s)=MONTH(EDATE(TODAY(),0-1)),YEAR(%s)=YEAR(EDATE(TODAY(),0-1)))', + Conditional::TIMEPERIOD_THIS_MONTH => 'AND(MONTH(%s)=MONTH(TODAY()),YEAR(%s)=YEAR(TODAY()))', + Conditional::TIMEPERIOD_NEXT_MONTH => 'AND(MONTH(%s)=MONTH(EDATE(TODAY(),0+1)),YEAR(%s)=YEAR(EDATE(TODAY(),0+1)))', + ]; + + /** @var string */ + protected $operator; + + public function __construct(string $cellRange) + { + parent::__construct($cellRange); + } + + protected function operator(string $operator): void + { + $this->operator = $operator; + } + + protected function setExpression(): void + { + $referenceCount = substr_count(self::EXPRESSIONS[$this->operator], '%s'); + $references = array_fill(0, $referenceCount, $this->referenceCell); + $this->expression = sprintf(self::EXPRESSIONS[$this->operator], ...$references); + } + + public function getConditional(): Conditional + { + $this->setExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType(Conditional::CONDITION_TIMEPERIOD); + $conditional->setText($this->operator); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ($conditional->getConditionType() !== Conditional::CONDITION_TIMEPERIOD) { + throw new Exception('Conditional is not a Date Value CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->operator = $conditional->getText(); + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!isset(self::MAGIC_OPERATIONS[$methodName])) { + throw new Exception('Invalid Operation for Date Value CF Rule Wizard'); + } + + $this->operator(self::MAGIC_OPERATIONS[$methodName]); + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php new file mode 100644 index 00000000000..3f063fefd52 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Duplicates.php @@ -0,0 +1,78 @@ + false, + 'unique' => true, + ]; + + /** + * @var bool + */ + protected $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + public function getConditional(): Conditional + { + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_UNIQUE : Conditional::CONDITION_DUPLICATES + ); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_DUPLICATES && + $conditional->getConditionType() !== Conditional::CONDITION_UNIQUE + ) { + throw new Exception('Conditional is not a Duplicates CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_UNIQUE; + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Errors CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php new file mode 100644 index 00000000000..56b9086c8d0 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Errors.php @@ -0,0 +1,95 @@ + false, + 'isError' => true, + ]; + + protected const EXPRESSIONS = [ + Wizard::NOT_ERRORS => 'NOT(ISERROR(%s))', + Wizard::ERRORS => 'ISERROR(%s)', + ]; + + /** + * @var bool + */ + protected $inverse; + + public function __construct(string $cellRange, bool $inverse = false) + { + parent::__construct($cellRange); + $this->inverse = $inverse; + } + + protected function inverse(bool $inverse): void + { + $this->inverse = $inverse; + } + + protected function getExpression(): void + { + $this->expression = sprintf( + self::EXPRESSIONS[$this->inverse ? Wizard::ERRORS : Wizard::NOT_ERRORS], + $this->referenceCell + ); + } + + public function getConditional(): Conditional + { + $this->getExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType( + $this->inverse ? Conditional::CONDITION_CONTAINSERRORS : Conditional::CONDITION_NOTCONTAINSERRORS + ); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ( + $conditional->getConditionType() !== Conditional::CONDITION_CONTAINSERRORS && + $conditional->getConditionType() !== Conditional::CONDITION_NOTCONTAINSERRORS + ) { + throw new Exception('Conditional is not an Errors CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->inverse = $conditional->getConditionType() === Conditional::CONDITION_CONTAINSERRORS; + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!array_key_exists($methodName, self::OPERATORS)) { + throw new Exception('Invalid Operation for Errors CF Rule Wizard'); + } + + $this->inverse(self::OPERATORS[$methodName]); + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php new file mode 100644 index 00000000000..ee71e34bda4 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/Expression.php @@ -0,0 +1,73 @@ +validateOperand($expression, Wizard::VALUE_TYPE_FORMULA); + $this->expression = $expression; + + return $this; + } + + public function getConditional(): Conditional + { + $expression = $this->adjustConditionsForCellReferences([$this->expression]); + + $conditional = new Conditional(); + $conditional->setConditionType(Conditional::CONDITION_EXPRESSION); + $conditional->setConditions($expression); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if ($conditional->getConditionType() !== Conditional::CONDITION_EXPRESSION) { + throw new Exception('Conditional is not an Expression CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + $wizard->expression = self::reverseAdjustCellRef((string) ($conditional->getConditions()[0]), $cellRange); + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if ($methodName !== 'formula') { + throw new Exception('Invalid Operation for Expression CF Rule Wizard'); + } + + $this->expression(...$arguments); + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php new file mode 100644 index 00000000000..4fa635bec5b --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/TextValue.php @@ -0,0 +1,163 @@ + Conditional::OPERATOR_CONTAINSTEXT, + 'doesntContain' => Conditional::OPERATOR_NOTCONTAINS, + 'doesNotContain' => Conditional::OPERATOR_NOTCONTAINS, + 'beginsWith' => Conditional::OPERATOR_BEGINSWITH, + 'startsWith' => Conditional::OPERATOR_BEGINSWITH, + 'endsWith' => Conditional::OPERATOR_ENDSWITH, + ]; + + protected const OPERATORS = [ + Conditional::OPERATOR_CONTAINSTEXT => Conditional::CONDITION_CONTAINSTEXT, + Conditional::OPERATOR_NOTCONTAINS => Conditional::CONDITION_NOTCONTAINSTEXT, + Conditional::OPERATOR_BEGINSWITH => Conditional::CONDITION_BEGINSWITH, + Conditional::OPERATOR_ENDSWITH => Conditional::CONDITION_ENDSWITH, + ]; + + protected const EXPRESSIONS = [ + Conditional::OPERATOR_CONTAINSTEXT => 'NOT(ISERROR(SEARCH(%s,%s)))', + Conditional::OPERATOR_NOTCONTAINS => 'ISERROR(SEARCH(%s,%s))', + Conditional::OPERATOR_BEGINSWITH => 'LEFT(%s,LEN(%s))=%s', + Conditional::OPERATOR_ENDSWITH => 'RIGHT(%s,LEN(%s))=%s', + ]; + + /** @var string */ + protected $operator; + + /** @var string */ + protected $operand; + + /** + * @var string + */ + protected $operandValueType; + + public function __construct(string $cellRange) + { + parent::__construct($cellRange); + } + + protected function operator(string $operator): void + { + if (!isset(self::OPERATORS[$operator])) { + throw new Exception('Invalid Operator for Text Value CF Rule Wizard'); + } + + $this->operator = $operator; + } + + protected function operand(string $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): void + { + if (is_string($operand)) { + $operand = $this->validateOperand($operand, $operandValueType); + } + + $this->operand = $operand; + $this->operandValueType = $operandValueType; + } + + protected function wrapValue(string $value): string + { + return '"' . $value . '"'; + } + + protected function setExpression(): void + { + $operand = $this->operandValueType === Wizard::VALUE_TYPE_LITERAL + ? $this->wrapValue(str_replace('"', '""', $this->operand)) + : $this->cellConditionCheck($this->operand); + + if ( + $this->operator === Conditional::OPERATOR_CONTAINSTEXT || + $this->operator === Conditional::OPERATOR_NOTCONTAINS + ) { + $this->expression = sprintf(self::EXPRESSIONS[$this->operator], $operand, $this->referenceCell); + } else { + $this->expression = sprintf(self::EXPRESSIONS[$this->operator], $this->referenceCell, $operand, $operand); + } + } + + public function getConditional(): Conditional + { + $this->setExpression(); + + $conditional = new Conditional(); + $conditional->setConditionType(self::OPERATORS[$this->operator]); + $conditional->setOperatorType($this->operator); + $conditional->setText( + $this->operandValueType !== Wizard::VALUE_TYPE_LITERAL + ? $this->cellConditionCheck($this->operand) + : $this->operand + ); + $conditional->setConditions([$this->expression]); + $conditional->setStyle($this->getStyle()); + $conditional->setStopIfTrue($this->getStopIfTrue()); + + return $conditional; + } + + public static function fromConditional(Conditional $conditional, string $cellRange = 'A1'): WizardInterface + { + if (!in_array($conditional->getConditionType(), self::OPERATORS, true)) { + throw new Exception('Conditional is not a Text Value CF Rule conditional'); + } + + $wizard = new self($cellRange); + $wizard->operator = (string) array_search($conditional->getConditionType(), self::OPERATORS, true); + $wizard->style = $conditional->getStyle(); + $wizard->stopIfTrue = $conditional->getStopIfTrue(); + + // Best-guess to try and identify if the text is a string literal, a cell reference or a formula? + $wizard->operandValueType = Wizard::VALUE_TYPE_LITERAL; + $condition = $conditional->getText(); + if (is_string($condition) && array_key_exists($condition, Calculation::$excelConstants)) { + $condition = Calculation::$excelConstants[$condition]; + } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '$/i', $condition)) { + $wizard->operandValueType = Wizard::VALUE_TYPE_CELL; + $condition = self::reverseAdjustCellRef($condition, $cellRange); + } elseif ( + preg_match('/\(\)/', $condition) || + preg_match('/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', $condition) + ) { + $wizard->operandValueType = Wizard::VALUE_TYPE_FORMULA; + } + $wizard->operand = $condition; + + return $wizard; + } + + /** + * @param string $methodName + * @param mixed[] $arguments + */ + public function __call($methodName, $arguments): self + { + if (!isset(self::MAGIC_OPERATIONS[$methodName])) { + throw new Exception('Invalid Operation for Text Value CF Rule Wizard'); + } + + $this->operator(self::MAGIC_OPERATIONS[$methodName]); + $this->operand(...$arguments); + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php new file mode 100644 index 00000000000..3eb7d54eabb --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardAbstract.php @@ -0,0 +1,197 @@ +setCellRange($cellRange); + } + + public function getCellRange(): string + { + return $this->cellRange; + } + + public function setCellRange(string $cellRange): void + { + $this->cellRange = $cellRange; + $this->setReferenceCellForExpressions($cellRange); + } + + protected function setReferenceCellForExpressions(string $conditionalRange): void + { + $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($conditionalRange))); + [$this->referenceCell] = $conditionalRange[0]; + + [$this->referenceColumn, $this->referenceRow] = Coordinate::indexesFromString($this->referenceCell); + } + + public function getStopIfTrue(): bool + { + return $this->stopIfTrue; + } + + public function setStopIfTrue(bool $stopIfTrue): void + { + $this->stopIfTrue = $stopIfTrue; + } + + public function getStyle(): Style + { + return $this->style ?? new Style(false, true); + } + + public function setStyle(Style $style): void + { + $this->style = $style; + } + + protected function validateOperand(string $operand, string $operandValueType = Wizard::VALUE_TYPE_LITERAL): string + { + if ( + $operandValueType === Wizard::VALUE_TYPE_LITERAL && + substr($operand, 0, 1) === '"' && + substr($operand, -1) === '"' + ) { + $operand = str_replace('""', '"', substr($operand, 1, -1)); + } elseif ($operandValueType === Wizard::VALUE_TYPE_FORMULA && substr($operand, 0, 1) === '=') { + $operand = substr($operand, 1); + } + + return $operand; + } + + protected static function reverseCellAdjustment(array $matches, int $referenceColumn, int $referenceRow): string + { + $worksheet = $matches[1]; + $column = $matches[6]; + $row = $matches[7]; + + if (strpos($column, '$') === false) { + $column = Coordinate::columnIndexFromString($column); + $column -= $referenceColumn - 1; + $column = Coordinate::stringFromColumnIndex($column); + } + + if (strpos($row, '$') === false) { + $row -= $referenceRow - 1; + } + + return "{$worksheet}{$column}{$row}"; + } + + public static function reverseAdjustCellRef(string $condition, string $cellRange): string + { + $conditionalRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellRange))); + [$referenceCell] = $conditionalRange[0]; + [$referenceColumnIndex, $referenceRow] = Coordinate::indexesFromString($referenceCell); + + $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition); + $i = false; + foreach ($splitCondition as &$value) { + // Only count/replace in alternating array entries (ie. not in quoted strings) + if ($i = !$i) { + $value = (string) preg_replace_callback( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', + function ($matches) use ($referenceColumnIndex, $referenceRow) { + return self::reverseCellAdjustment($matches, $referenceColumnIndex, $referenceRow); + }, + $value + ); + } + } + unset($value); + + // Then rebuild the condition string to return it + return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition); + } + + protected function conditionCellAdjustment(array $matches): string + { + $worksheet = $matches[1]; + $column = $matches[6]; + $row = $matches[7]; + + if (strpos($column, '$') === false) { + $column = Coordinate::columnIndexFromString($column); + $column += $this->referenceColumn - 1; + $column = Coordinate::stringFromColumnIndex($column); + } + + if (strpos($row, '$') === false) { + $row += $this->referenceRow - 1; + } + + return "{$worksheet}{$column}{$row}"; + } + + protected function cellConditionCheck(string $condition): string + { + $splitCondition = explode(Calculation::FORMULA_STRING_QUOTE, $condition); + $i = false; + foreach ($splitCondition as &$value) { + // Only count/replace in alternating array entries (ie. not in quoted strings) + if ($i = !$i) { + $value = (string) preg_replace_callback( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/i', + [$this, 'conditionCellAdjustment'], + $value + ); + } + } + unset($value); + + // Then rebuild the condition string to return it + return implode(Calculation::FORMULA_STRING_QUOTE, $splitCondition); + } + + protected function adjustConditionsForCellReferences(array $conditions): array + { + return array_map( + [$this, 'cellConditionCheck'], + $conditions + ); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardInterface.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardInterface.php new file mode 100644 index 00000000000..10ad57b20ea --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/ConditionalFormatting/Wizard/WizardInterface.php @@ -0,0 +1,25 @@ +setName($styleArray['name']); } + if (isset($styleArray['latin'])) { + $this->setLatin($styleArray['latin']); + } + if (isset($styleArray['eastAsian'])) { + $this->setEastAsian($styleArray['eastAsian']); + } + if (isset($styleArray['complexScript'])) { + $this->setComplexScript($styleArray['complexScript']); + } if (isset($styleArray['bold'])) { $this->setBold($styleArray['bold']); } @@ -213,6 +250,33 @@ class Font extends Supervisor return $this->name; } + public function getLatin(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getLatin(); + } + + return $this->latin; + } + + public function getEastAsian(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getEastAsian(); + } + + return $this->eastAsian; + } + + public function getComplexScript(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getComplexScript(); + } + + return $this->complexScript; + } + /** * Set Name. * @@ -235,6 +299,60 @@ class Font extends Supervisor return $this; } + public function setLatin(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if (!$this->isSupervisor) { + $this->latin = $fontname; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['latin' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function setEastAsian(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if (!$this->isSupervisor) { + $this->eastAsian = $fontname; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['eastAsian' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function setComplexScript(string $fontname): self + { + if ($fontname == '') { + $fontname = 'Calibri'; + } + if (!$this->isSupervisor) { + $this->complexScript = $fontname; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['complexScript' => $fontname]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + /** * Get Size. * @@ -256,7 +374,7 @@ class Font extends Supervisor * * @return $this */ - public function setSize($sizeInPoints) + public function setSize($sizeInPoints, bool $nullOk = false) { if (is_string($sizeInPoints) || is_int($sizeInPoints)) { $sizeInPoints = (float) $sizeInPoints; // $pValue = 0 if given string is not numeric @@ -265,7 +383,9 @@ class Font extends Supervisor // Size must be a positive floating point number // ECMA-376-1:2016, part 1, chapter 18.4.11 sz (Font Size), p. 1536 if (!is_float($sizeInPoints) || !($sizeInPoints > 0)) { - $sizeInPoints = 10.0; + if (!$nullOk || $sizeInPoints !== null) { + $sizeInPoints = 10.0; + } } if ($this->isSupervisor) { @@ -418,6 +538,102 @@ class Font extends Supervisor return $this; } + public function getBaseLine(): int + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getBaseLine(); + } + + return $this->baseLine; + } + + public function setBaseLine(int $baseLine): self + { + if (!$this->isSupervisor) { + $this->baseLine = $baseLine; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['baseLine' => $baseLine]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function getStrikeType(): string + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getStrikeType(); + } + + return $this->strikeType; + } + + public function setStrikeType(string $strikeType): self + { + if (!$this->isSupervisor) { + $this->strikeType = $strikeType; + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['strikeType' => $strikeType]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function getUnderlineColor(): ?ChartColor + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getUnderlineColor(); + } + + return $this->underlineColor; + } + + public function setUnderlineColor(array $colorArray): self + { + if (!$this->isSupervisor) { + $this->underlineColor = new ChartColor($colorArray); + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['underlineColor' => $colorArray]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + + public function getChartColor(): ?ChartColor + { + if ($this->isSupervisor) { + return $this->getSharedComponent()->getChartColor(); + } + + return $this->chartColor; + } + + public function setChartColor(array $colorArray): self + { + if (!$this->isSupervisor) { + $this->chartColor = new ChartColor($colorArray); + } else { + // should never be true + // @codeCoverageIgnoreStart + $styleArray = $this->getStyleArray(['chartColor' => $colorArray]); + $this->getActiveSheet()->getStyle($this->getSelectedCells())->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd + } + + return $this; + } + /** * Get Underline. * @@ -525,6 +741,18 @@ class Font extends Supervisor return $this; } + private function hashChartColor(?ChartColor $underlineColor): string + { + if ($underlineColor === null) { + return ''; + } + + return + $underlineColor->getValue() + . $underlineColor->getType() + . (string) $underlineColor->getAlpha(); + } + /** * Get hash code. * @@ -546,6 +774,18 @@ class Font extends Supervisor $this->underline . ($this->strikethrough ? 't' : 'f') . $this->color->getHashCode() . + implode( + '*', + [ + $this->latin, + $this->eastAsian, + $this->complexScript, + $this->strikeType, + $this->hashChartColor($this->chartColor), + $this->hashChartColor($this->underlineColor), + (string) $this->baseLine, + ] + ) . __CLASS__ ); } @@ -553,15 +793,22 @@ class Font extends Supervisor protected function exportArray1(): array { $exportedArray = []; + $this->exportArray2($exportedArray, 'baseLine', $this->getBaseLine()); $this->exportArray2($exportedArray, 'bold', $this->getBold()); + $this->exportArray2($exportedArray, 'chartColor', $this->getChartColor()); $this->exportArray2($exportedArray, 'color', $this->getColor()); + $this->exportArray2($exportedArray, 'complexScript', $this->getComplexScript()); + $this->exportArray2($exportedArray, 'eastAsian', $this->getEastAsian()); $this->exportArray2($exportedArray, 'italic', $this->getItalic()); + $this->exportArray2($exportedArray, 'latin', $this->getLatin()); $this->exportArray2($exportedArray, 'name', $this->getName()); $this->exportArray2($exportedArray, 'size', $this->getSize()); $this->exportArray2($exportedArray, 'strikethrough', $this->getStrikethrough()); + $this->exportArray2($exportedArray, 'strikeType', $this->getStrikeType()); $this->exportArray2($exportedArray, 'subscript', $this->getSubscript()); $this->exportArray2($exportedArray, 'superscript', $this->getSuperscript()); $this->exportArray2($exportedArray, 'underline', $this->getUnderline()); + $this->exportArray2($exportedArray, 'underlineColor', $this->getUnderlineColor()); return $exportedArray; } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php index 536b1d54db1..6f552cb3612 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat.php @@ -10,11 +10,13 @@ class NumberFormat extends Supervisor const FORMAT_TEXT = '@'; const FORMAT_NUMBER = '0'; + const FORMAT_NUMBER_0 = '0.0'; const FORMAT_NUMBER_00 = '0.00'; const FORMAT_NUMBER_COMMA_SEPARATED1 = '#,##0.00'; const FORMAT_NUMBER_COMMA_SEPARATED2 = '#,##0.00_-'; const FORMAT_PERCENTAGE = '0%'; + const FORMAT_PERCENTAGE_0 = '0.0%'; const FORMAT_PERCENTAGE_00 = '0.00%'; const FORMAT_DATE_YYYYMMDD2 = 'yyyy-mm-dd'; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php index db18e636fe3..6c4d9d6ba06 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php @@ -8,10 +8,8 @@ class DateFormatter { /** * Search/replace values to convert Excel date/time format masks to PHP format masks. - * - * @var array */ - private static $dateFormatReplacements = [ + private const DATE_FORMAT_REPLACEMENTS = [ // first remove escapes related to non-format characters '\\' => '', // 12-hour suffix @@ -32,10 +30,6 @@ class DateFormatter // It isn't perfect, but the best way I know how ':mm' => ':i', 'mm:' => 'i:', - // month leading zero - 'mm' => 'm', - // month no leading zero - 'm' => 'n', // full day of week name 'dddd' => 'l', // short day of week name @@ -44,85 +38,144 @@ class DateFormatter 'dd' => 'd', // days no leading zero 'd' => 'j', - // seconds - 'ss' => 's', // fractional seconds - no php equivalent '.s' => '', ]; /** * Search/replace values to convert Excel date/time format masks hours to PHP format masks (24 hr clock). - * - * @var array */ - private static $dateFormatReplacements24 = [ + private const DATE_FORMAT_REPLACEMENTS24 = [ 'hh' => 'H', 'h' => 'G', + // month leading zero + 'mm' => 'm', + // month no leading zero + 'm' => 'n', + // seconds + 'ss' => 's', ]; /** * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock). - * - * @var array */ - private static $dateFormatReplacements12 = [ + private const DATE_FORMAT_REPLACEMENTS12 = [ 'hh' => 'h', 'h' => 'g', + // month leading zero + 'mm' => 'm', + // month no leading zero + 'm' => 'n', + // seconds + 'ss' => 's', ]; + private const HOURS_IN_DAY = 24; + private const MINUTES_IN_DAY = 60 * self::HOURS_IN_DAY; + private const SECONDS_IN_DAY = 60 * self::MINUTES_IN_DAY; + private const INTERVAL_PRECISION = 10; + private const INTERVAL_LEADING_ZERO = [ + '[hh]', + '[mm]', + '[ss]', + ]; + private const INTERVAL_ROUND_PRECISION = [ + // hours and minutes truncate + '[h]' => self::INTERVAL_PRECISION, + '[hh]' => self::INTERVAL_PRECISION, + '[m]' => self::INTERVAL_PRECISION, + '[mm]' => self::INTERVAL_PRECISION, + // seconds round + '[s]' => 0, + '[ss]' => 0, + ]; + private const INTERVAL_MULTIPLIER = [ + '[h]' => self::HOURS_IN_DAY, + '[hh]' => self::HOURS_IN_DAY, + '[m]' => self::MINUTES_IN_DAY, + '[mm]' => self::MINUTES_IN_DAY, + '[s]' => self::SECONDS_IN_DAY, + '[ss]' => self::SECONDS_IN_DAY, + ]; + + /** @param mixed $value */ + private static function tryInterval(bool &$seekingBracket, string &$block, $value, string $format): void + { + if ($seekingBracket) { + if (false !== strpos($block, $format)) { + $hours = (string) (int) round( + self::INTERVAL_MULTIPLIER[$format] * $value, + self::INTERVAL_ROUND_PRECISION[$format] + ); + if (strlen($hours) === 1 && in_array($format, self::INTERVAL_LEADING_ZERO, true)) { + $hours = "0$hours"; + } + $block = str_replace($format, $hours, $block); + $seekingBracket = false; + } + } + } + + /** @param mixed $value */ public static function format($value, string $format): string { // strip off first part containing e.g. [$-F800] or [$USD-409] // general syntax: [$-] // language info is in hexadecimal // strip off chinese part like [DBNum1][$-804] - $format = preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format); + $format = (string) preg_replace('/^(\[DBNum\d\])*(\[\$[^\]]*\])/i', '', $format); // OpenOffice.org uses upper-case number formats, e.g. 'YYYY', convert to lower-case; // but we don't want to change any quoted strings - $format = preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', ['self', 'setLowercaseCallback'], $format); + /** @var callable */ + $callable = [self::class, 'setLowercaseCallback']; + $format = (string) preg_replace_callback('/(?:^|")([^"]*)(?:$|")/', $callable, $format); // Only process the non-quoted blocks for date format characters + $blocks = explode('"', $format); foreach ($blocks as $key => &$block) { if ($key % 2 == 0) { - $block = strtr($block, self::$dateFormatReplacements); + $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS); if (!strpos($block, 'A')) { // 24-hour time format // when [h]:mm format, the [h] should replace to the hours of the value * 24 - if (false !== strpos($block, '[h]')) { - $hours = (int) ($value * 24); - $block = str_replace('[h]', $hours, $block); - - continue; - } - $block = strtr($block, self::$dateFormatReplacements24); + $seekingBracket = true; + self::tryInterval($seekingBracket, $block, $value, '[h]'); + self::tryInterval($seekingBracket, $block, $value, '[hh]'); + self::tryInterval($seekingBracket, $block, $value, '[mm]'); + self::tryInterval($seekingBracket, $block, $value, '[m]'); + self::tryInterval($seekingBracket, $block, $value, '[s]'); + self::tryInterval($seekingBracket, $block, $value, '[ss]'); + $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS24); } else { // 12-hour time format - $block = strtr($block, self::$dateFormatReplacements12); + $block = strtr($block, self::DATE_FORMAT_REPLACEMENTS12); } } } $format = implode('"', $blocks); // escape any quoted characters so that DateTime format() will render them correctly - $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $format); + /** @var callable */ + $callback = [self::class, 'escapeQuotesCallback']; + $format = (string) preg_replace_callback('/"(.*)"/U', $callback, $format); $dateObj = Date::excelToDateTimeObject($value); // If the colon preceding minute had been quoted, as happens in // Excel 2003 XML formats, m will not have been changed to i above. // Change it now. - $format = \preg_replace('/\\\\:m/', ':i', $format); + $format = (string) \preg_replace('/\\\\:m/', ':i', $format); return $dateObj->format($format); } - private static function setLowercaseCallback($matches): string + private static function setLowercaseCallback(array $matches): string { return mb_strtolower($matches[0]); } - private static function escapeQuotesCallback($matches): string + private static function escapeQuotesCallback(array $matches): string { return '\\' . implode('\\', str_split($matches[1])); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 01407e64232..be195a88032 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -51,12 +51,12 @@ class Formatter for ($idx = 0; $idx < $cnt; ++$idx) { if (preg_match($color_regex, $sections[$idx], $matches)) { $colors[$idx] = $matches[0]; - $sections[$idx] = preg_replace($color_regex, '', $sections[$idx]); + $sections[$idx] = (string) preg_replace($color_regex, '', $sections[$idx]); } if (preg_match($cond_regex, $sections[$idx], $matches)) { $condops[$idx] = $matches[1]; $condvals[$idx] = $matches[2]; - $sections[$idx] = preg_replace($cond_regex, '', $sections[$idx]); + $sections[$idx] = (string) preg_replace($cond_regex, '', $sections[$idx]); } } $color = $colors[0]; @@ -112,7 +112,10 @@ class Formatter return $value; } - $format = preg_replace_callback( + // Ignore square-$-brackets prefix in format string, like "[$-411]ge.m.d", "[$-010419]0%", etc + $format = (string) preg_replace('/^\[\$-[^\]]*\]/', '', $format); + + $format = (string) preg_replace_callback( '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', function ($matches) { return str_replace('.', chr(0x00), $matches[0]); @@ -121,7 +124,7 @@ class Formatter ); // Convert any other escaped characters to quoted strings, e.g. (\T to "T") - $format = preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); + $format = (string) preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/ui', '"${2}"', $format); // Get the sections, there can be up to four sections, separated with a semi-colon (but only if not a quoted literal) $sections = preg_split('/(;)(?=(?:[^"]|"[^"]*")*$)/u', $format); @@ -130,7 +133,7 @@ class Formatter // In Excel formats, "_" is used to add spacing, // The following character indicates the size of the spacing, which we can't do in HTML, so we just use a standard space - $format = preg_replace('/_.?/ui', ' ', $format); + $format = (string) preg_replace('/_.?/ui', ' ', $format); // Let's begin inspecting the format and converting the value to a formatted string diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php index 46f27cc33ff..d1fc89fd000 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -26,9 +26,12 @@ class FractionFormatter extends BaseFormatter $decimalLength = strlen($decimalPart); $decimalDivisor = 10 ** $decimalLength; + /** @var float */ $GCD = MathTrig\Gcd::evaluate($decimalPart, $decimalDivisor); + /** @var float */ + $decimalPartx = $decimalPart; - $adjustedDecimalPart = $decimalPart / $GCD; + $adjustedDecimalPart = $decimalPartx / $GCD; $adjustedDecimalDivisor = $decimalDivisor / $GCD; if ((strpos($format, '0') !== false)) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php index cf1731ec85a..07aaff1f7aa 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -20,7 +20,8 @@ class PercentageFormatter extends BaseFormatter $format = str_replace('%', '%%', $format); $wholePartSize = strlen((string) floor($value)); - $decimalPartSize = $placeHolders = 0; + $decimalPartSize = 0; + $placeHolders = ''; // Number of decimals if (preg_match('/\.([?0]+)/u', $format, $matches)) { $decimalPartSize = strlen($matches[1]); @@ -29,14 +30,18 @@ class PercentageFormatter extends BaseFormatter $placeHolders = str_repeat(' ', strlen($matches[1]) - $decimalPartSize); } // Number of digits to display before the decimal - if (preg_match('/([#0,]+)\./u', $format, $matches)) { - $wholePartSize = max($wholePartSize, strlen($matches[1])); + if (preg_match('/([#0,]+)\.?/u', $format, $matches)) { + $firstZero = preg_replace('/^[#,]*/', '', $matches[1]); + $wholePartSize = max($wholePartSize, strlen($firstZero)); } - $wholePartSize += $decimalPartSize; - $replacement = "{$wholePartSize}.{$decimalPartSize}"; - $mask = preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); + $wholePartSize += $decimalPartSize + (int) ($decimalPartSize > 0); + $replacement = "0{$wholePartSize}.{$decimalPartSize}"; + $mask = (string) preg_replace('/[#0,]+\.?[?#0,]*/ui', "%{$replacement}f{$placeHolders}", $format); - return sprintf($mask, $value); + /** @var float */ + $valueFloat = $value; + + return sprintf($mask, round($valueFloat, $decimalPartSize)); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php index fdb15451bab..5ea7059758e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Style/Style.php @@ -78,9 +78,7 @@ class Style extends Supervisor * @see Style::applyFromArray() * @see Style::getHashCode() * - * @phpstan-var null|array{styleByHash: array, hashByObjId: array} - * - * @var array + * @var ?array */ private static $cachedStyles; @@ -423,8 +421,10 @@ class Style extends Supervisor // Handle bug in PHPStan, see https://github.com/phpstan/phpstan/issues/5805 // $newStyle should always be defined. // This block might not be needed in the future + // @codeCoverageIgnoreStart $newStyle = clone $style; $newStyle->applyFromArray($styleArray); + // @codeCoverageIgnoreEnd } // we don't have such a cell Xf, need to add diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 893243dfd02..05b2e9a06cb 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -7,6 +7,7 @@ use DateTimeZone; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -50,9 +51,18 @@ class AutoFilter /** * Create a new AutoFilter. + * + * @param AddressRange|array|string $range + * A simple string containing a Cell range like 'A1:E10' is permitted + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. */ - public function __construct(string $range = '', ?Worksheet $worksheet = null) + public function __construct($range = '', ?Worksheet $worksheet = null) { + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } + $this->range = $range; $this->workSheet = $worksheet; } @@ -92,12 +102,19 @@ class AutoFilter /** * Set AutoFilter Cell Range. + * + * @param AddressRange|array|string $range + * A simple string containing a Cell range like 'A1:E10' is permitted + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. */ - public function setRange(string $range): self + public function setRange($range = ''): self { $this->evaluated = false; // extract coordinate - [$worksheet, $range] = Worksheet::extractSheetTitle($range, true); + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } if (empty($range)) { // Discard all column rules $this->columns = []; @@ -128,7 +145,7 @@ class AutoFilter $this->evaluated = false; if ($this->workSheet !== null) { $thisrange = $this->range; - $range = preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange) ?? ''; + $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); if ($range !== $thisrange) { $this->setRange($range); } @@ -386,7 +403,7 @@ class AutoFilter /** @var string */ $ruleOperator = $rule['operator']; /** @var string */ - $cellValueString = $cellValue; + $cellValueString = $cellValue ?? ''; $retVal = false; if (is_numeric($ruleValue)) { @@ -931,6 +948,9 @@ class AutoFilter $averageFormula = '=AVERAGE(' . $columnID . ($rangeStart[1] + 1) . ':' . $columnID . $rangeEnd[1] . ')'; $spreadsheet = ($this->workSheet === null) ? null : $this->workSheet->getParent(); $average = Calculation::getInstance($spreadsheet)->calculateFormula($averageFormula, null, $this->workSheet->getCell('A1')); + while (is_array($average)) { + $average = array_pop($average); + } // Set above/below rule based on greaterThan or LessTan $operator = ($dynamicRuleType === Rule::AUTOFILTER_RULETYPE_DYNAMIC_ABOVEAVERAGE) ? Rule::AUTOFILTER_COLUMN_RULE_GREATERTHAN diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php index 2e3ea65bf3b..076292f37af 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/AutoFilter/Column.php @@ -243,7 +243,7 @@ class Column * Set An AutoFilter Attribute. * * @param string $name Attribute Name - * @param string $value Attribute Value + * @param int|string $value Attribute Value * * @return $this */ diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 46f061589f8..369e416205e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -8,6 +8,22 @@ use PhpOffice\PhpSpreadsheet\IComparable; class BaseDrawing implements IComparable { + const EDIT_AS_ABSOLUTE = 'absolute'; + const EDIT_AS_ONECELL = 'oneCell'; + const EDIT_AS_TWOCELL = 'twoCell'; + private const VALID_EDIT_AS = [ + self::EDIT_AS_ABSOLUTE, + self::EDIT_AS_ONECELL, + self::EDIT_AS_TWOCELL, + ]; + + /** + * The editAs attribute, used only with two cell anchor. + * + * @var string + */ + protected $editAs = ''; + /** * Image counter. * @@ -27,14 +43,14 @@ class BaseDrawing implements IComparable * * @var string */ - protected $name; + protected $name = ''; /** * Description. * * @var string */ - protected $description; + protected $description = ''; /** * Worksheet. @@ -48,49 +64,84 @@ class BaseDrawing implements IComparable * * @var string */ - protected $coordinates; + protected $coordinates = 'A1'; /** * Offset X. * * @var int */ - protected $offsetX; + protected $offsetX = 0; /** * Offset Y. * * @var int */ - protected $offsetY; + protected $offsetY = 0; + + /** + * Coordinates2. + * + * @var string + */ + protected $coordinates2 = ''; + + /** + * Offset X2. + * + * @var int + */ + protected $offsetX2 = 0; + + /** + * Offset Y2. + * + * @var int + */ + protected $offsetY2 = 0; /** * Width. * * @var int */ - protected $width; + protected $width = 0; /** * Height. * * @var int */ - protected $height; + protected $height = 0; + + /** + * Pixel width of image. See $width for the size the Drawing will be in the sheet. + * + * @var int + */ + protected $imageWidth = 0; + + /** + * Pixel width of image. See $height for the size the Drawing will be in the sheet. + * + * @var int + */ + protected $imageHeight = 0; /** * Proportional resize. * * @var bool */ - protected $resizeProportional; + protected $resizeProportional = true; /** * Rotation. * * @var int */ - protected $rotation; + protected $rotation = 0; /** * Shadow. @@ -111,7 +162,7 @@ class BaseDrawing implements IComparable * * @var int */ - protected $type; + protected $type = IMAGETYPE_UNKNOWN; /** * Create a new BaseDrawing. @@ -119,88 +170,43 @@ class BaseDrawing implements IComparable public function __construct() { // Initialise values - $this->name = ''; - $this->description = ''; - $this->worksheet = null; - $this->coordinates = 'A1'; - $this->offsetX = 0; - $this->offsetY = 0; - $this->width = 0; - $this->height = 0; - $this->resizeProportional = true; - $this->rotation = 0; - $this->shadow = new Drawing\Shadow(); - $this->type = IMAGETYPE_UNKNOWN; + $this->setShadow(); // Set image index ++self::$imageCounter; $this->imageIndex = self::$imageCounter; } - /** - * Get image index. - * - * @return int - */ - public function getImageIndex() + public function getImageIndex(): int { return $this->imageIndex; } - /** - * Get Name. - * - * @return string - */ - public function getName() + public function getName(): string { return $this->name; } - /** - * Set Name. - * - * @param string $name - * - * @return $this - */ - public function setName($name) + public function setName(string $name): self { $this->name = $name; return $this; } - /** - * Get Description. - * - * @return string - */ - public function getDescription() + public function getDescription(): string { return $this->description; } - /** - * Set Description. - * - * @param string $description - * - * @return $this - */ - public function setDescription($description) + public function setDescription(string $description): self { $this->description = $description; return $this; } - /** - * Get Worksheet. - * - * @return null|Worksheet - */ - public function getWorksheet() + public function getWorksheet(): ?Worksheet { return $this->worksheet; } @@ -209,16 +215,16 @@ class BaseDrawing implements IComparable * Set Worksheet. * * @param bool $overrideOld If a Worksheet has already been assigned, overwrite it and remove image from old Worksheet? - * - * @return $this */ - public function setWorksheet(?Worksheet $worksheet = null, $overrideOld = false) + public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = false): self { if ($this->worksheet === null) { // Add drawing to \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet - $this->worksheet = $worksheet; - $this->worksheet->getCell($this->coordinates); - $this->worksheet->getDrawingCollection()->append($this); + if ($worksheet !== null) { + $this->worksheet = $worksheet; + $this->worksheet->getCell($this->coordinates); + $this->worksheet->getDrawingCollection()->append($this); + } } else { if ($overrideOld) { // Remove drawing from old \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet @@ -243,96 +249,84 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get Coordinates. - * - * @return string - */ - public function getCoordinates() + public function getCoordinates(): string { return $this->coordinates; } - /** - * Set Coordinates. - * - * @param string $coordinates eg: 'A1' - * - * @return $this - */ - public function setCoordinates($coordinates) + public function setCoordinates(string $coordinates): self { $this->coordinates = $coordinates; return $this; } - /** - * Get OffsetX. - * - * @return int - */ - public function getOffsetX() + public function getOffsetX(): int { return $this->offsetX; } - /** - * Set OffsetX. - * - * @param int $offsetX - * - * @return $this - */ - public function setOffsetX($offsetX) + public function setOffsetX(int $offsetX): self { $this->offsetX = $offsetX; return $this; } - /** - * Get OffsetY. - * - * @return int - */ - public function getOffsetY() + public function getOffsetY(): int { return $this->offsetY; } - /** - * Set OffsetY. - * - * @param int $offsetY - * - * @return $this - */ - public function setOffsetY($offsetY) + public function setOffsetY(int $offsetY): self { $this->offsetY = $offsetY; return $this; } - /** - * Get Width. - * - * @return int - */ - public function getWidth() + public function getCoordinates2(): string + { + return $this->coordinates2; + } + + public function setCoordinates2(string $coordinates2): self + { + $this->coordinates2 = $coordinates2; + + return $this; + } + + public function getOffsetX2(): int + { + return $this->offsetX2; + } + + public function setOffsetX2(int $offsetX2): self + { + $this->offsetX2 = $offsetX2; + + return $this; + } + + public function getOffsetY2(): int + { + return $this->offsetY2; + } + + public function setOffsetY2(int $offsetY2): self + { + $this->offsetY2 = $offsetY2; + + return $this; + } + + public function getWidth(): int { return $this->width; } - /** - * Set Width. - * - * @param int $width - * - * @return $this - */ - public function setWidth($width) + public function setWidth(int $width): self { // Resize proportional? if ($this->resizeProportional && $width != 0) { @@ -346,24 +340,12 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get Height. - * - * @return int - */ - public function getHeight() + public function getHeight(): int { return $this->height; } - /** - * Set Height. - * - * @param int $height - * - * @return $this - */ - public function setHeight($height) + public function setHeight(int $height): self { // Resize proportional? if ($this->resizeProportional && $height != 0) { @@ -386,14 +368,9 @@ class BaseDrawing implements IComparable * $objDrawing->setWidthAndHeight(160,120); * * - * @param int $width - * @param int $height - * - * @return $this - * * @author Vincent@luo MSN:kele_100@hotmail.com */ - public function setWidthAndHeight($width, $height) + public function setWidthAndHeight(int $width, int $height): self { $xratio = $width / ($this->width != 0 ? $this->width : 1); $yratio = $height / ($this->height != 0 ? $this->height : 1); @@ -413,72 +390,38 @@ class BaseDrawing implements IComparable return $this; } - /** - * Get ResizeProportional. - * - * @return bool - */ - public function getResizeProportional() + public function getResizeProportional(): bool { return $this->resizeProportional; } - /** - * Set ResizeProportional. - * - * @param bool $resizeProportional - * - * @return $this - */ - public function setResizeProportional($resizeProportional) + public function setResizeProportional(bool $resizeProportional): self { $this->resizeProportional = $resizeProportional; return $this; } - /** - * Get Rotation. - * - * @return int - */ - public function getRotation() + public function getRotation(): int { return $this->rotation; } - /** - * Set Rotation. - * - * @param int $rotation - * - * @return $this - */ - public function setRotation($rotation) + public function setRotation(int $rotation): self { $this->rotation = $rotation; return $this; } - /** - * Get Shadow. - * - * @return Drawing\Shadow - */ - public function getShadow() + public function getShadow(): Drawing\Shadow { return $this->shadow; } - /** - * Set Shadow. - * - * @return $this - */ - public function setShadow(?Drawing\Shadow $shadow = null) + public function setShadow(?Drawing\Shadow $shadow = null): self { - $this->shadow = $shadow; + $this->shadow = $shadow ?? new Drawing\Shadow(); return $this; } @@ -493,10 +436,13 @@ class BaseDrawing implements IComparable return md5( $this->name . $this->description . - $this->worksheet->getHashCode() . + (($this->worksheet === null) ? '' : $this->worksheet->getHashCode()) . $this->coordinates . $this->offsetX . $this->offsetY . + $this->coordinates2 . + $this->offsetX2 . + $this->offsetY2 . $this->width . $this->height . $this->rotation . @@ -527,10 +473,7 @@ class BaseDrawing implements IComparable $this->hyperlink = $hyperlink; } - /** - * @return null|Hyperlink - */ - public function getHyperlink() + public function getHyperlink(): ?Hyperlink { return $this->hyperlink; } @@ -540,15 +483,19 @@ class BaseDrawing implements IComparable */ protected function setSizesAndType(string $path): void { - if ($this->width == 0 && $this->height == 0 && $this->type == IMAGETYPE_UNKNOWN) { + if ($this->imageWidth === 0 && $this->imageHeight === 0 && $this->type === IMAGETYPE_UNKNOWN) { $imageData = getimagesize($path); if (is_array($imageData)) { - $this->width = $imageData[0]; - $this->height = $imageData[1]; + $this->imageWidth = $imageData[0]; + $this->imageHeight = $imageData[1]; $this->type = $imageData[2]; } } + if ($this->width === 0 && $this->height === 0) { + $this->width = $this->imageWidth; + $this->height = $this->imageHeight; + } } /** @@ -558,4 +505,31 @@ class BaseDrawing implements IComparable { return $this->type; } + + public function getImageWidth(): int + { + return $this->imageWidth; + } + + public function getImageHeight(): int + { + return $this->imageHeight; + } + + public function getEditAs(): string + { + return $this->editAs; + } + + public function setEditAs(string $editAs): self + { + $this->editAs = $editAs; + + return $this; + } + + public function validEditAs(): bool + { + return in_array($this->editAs, self::VALID_EDIT_AS, true); + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php index 444e3b1fae5..94877f6671b 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/CellIterator.php @@ -4,13 +4,19 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; use Iterator; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Collection\Cells; /** * @template TKey + * * @implements Iterator */ abstract class CellIterator implements Iterator { + public const TREAT_NULL_VALUE_AS_EMPTY_CELL = 1; + + public const TREAT_EMPTY_STRING_AS_EMPTY_CELL = 2; + /** * Worksheet to iterate. * @@ -18,6 +24,13 @@ abstract class CellIterator implements Iterator */ protected $worksheet; + /** + * Cell Collection to iterate. + * + * @var Cells + */ + protected $cellCollection; + /** * Iterate only existing cells. * @@ -31,7 +44,7 @@ abstract class CellIterator implements Iterator public function __destruct() { // @phpstan-ignore-next-line - $this->worksheet = null; + $this->worksheet = $this->cellCollection = null; } /** diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php index b6f30f1f984..e6e332ae748 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Column.php @@ -9,7 +9,7 @@ class Column * * @var Worksheet */ - private $parent; + private $worksheet; /** * Column index. @@ -23,10 +23,10 @@ class Column * * @param string $columnIndex */ - public function __construct(?Worksheet $parent = null, $columnIndex = 'A') + public function __construct(Worksheet $worksheet, $columnIndex = 'A') { // Set parent and column index - $this->parent = $parent; + $this->worksheet = $worksheet; $this->columnIndex = $columnIndex; } @@ -36,7 +36,7 @@ class Column public function __destruct() { // @phpstan-ignore-next-line - $this->parent = null; + $this->worksheet = null; } /** @@ -57,6 +57,53 @@ class Column */ public function getCellIterator($startRow = 1, $endRow = null) { - return new ColumnCellIterator($this->parent, $this->columnIndex, $startRow, $endRow); + return new ColumnCellIterator($this->worksheet, $this->columnIndex, $startRow, $endRow); + } + + /** + * Returns a boolean true if the column contains no cells. By default, this means that no cell records exist in the + * collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + + /** + * Returns bound worksheet. + */ + public function getWorksheet(): Worksheet + { + return $this->worksheet; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php index 9d0be5bd470..a518f59c4cc 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnCellIterator.php @@ -42,15 +42,16 @@ class ColumnCellIterator extends CellIterator /** * Create a new row iterator. * - * @param Worksheet $subject The worksheet to iterate over + * @param Worksheet $worksheet The worksheet to iterate over * @param string $columnIndex The column that we want to iterate * @param int $startRow The row number at which to start iterating * @param int $endRow Optionally, the row number at which to stop iterating */ - public function __construct(Worksheet $subject, $columnIndex = 'A', $startRow = 1, $endRow = null) + public function __construct(Worksheet $worksheet, $columnIndex = 'A', $startRow = 1, $endRow = null) { // Set subject - $this->worksheet = $subject; + $this->worksheet = $worksheet; + $this->cellCollection = $worksheet->getCellCollection(); $this->columnIndex = Coordinate::columnIndexFromString($columnIndex); $this->resetEnd($endRow); $this->resetStart($startRow); @@ -96,7 +97,10 @@ class ColumnCellIterator extends CellIterator */ public function seek(int $row = 1) { - if ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $row))) { + if ( + $this->onlyExistingCells && + (!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->columnIndex) . $row)) + ) { throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist'); } if (($row < $this->startRow) || ($row > $this->endRow)) { @@ -120,7 +124,11 @@ class ColumnCellIterator extends CellIterator */ public function current(): ?Cell { - return $this->worksheet->getCellByColumnAndRow($this->columnIndex, $this->currentRow); + $cellAddress = Coordinate::stringFromColumnIndex($this->columnIndex) . $this->currentRow; + + return $this->cellCollection->has($cellAddress) + ? $this->cellCollection->get($cellAddress) + : $this->worksheet->createNewCell($cellAddress); } /** @@ -136,12 +144,13 @@ class ColumnCellIterator extends CellIterator */ public function next(): void { + $columnAddress = Coordinate::stringFromColumnIndex($this->columnIndex); do { ++$this->currentRow; } while ( ($this->onlyExistingCells) && - (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->currentRow)) && - ($this->currentRow <= $this->endRow) + ($this->currentRow <= $this->endRow) && + (!$this->cellCollection->has($columnAddress . $this->currentRow)) ); } @@ -150,12 +159,13 @@ class ColumnCellIterator extends CellIterator */ public function prev(): void { + $columnAddress = Coordinate::stringFromColumnIndex($this->columnIndex); do { --$this->currentRow; } while ( ($this->onlyExistingCells) && - (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->currentRow)) && - ($this->currentRow >= $this->startRow) + ($this->currentRow >= $this->startRow) && + (!$this->cellCollection->has($columnAddress . $this->currentRow)) ); } @@ -173,14 +183,15 @@ class ColumnCellIterator extends CellIterator protected function adjustForExistingOnlyRange(): void { if ($this->onlyExistingCells) { + $columnAddress = Coordinate::stringFromColumnIndex($this->columnIndex); while ( - (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->startRow)) && + (!$this->cellCollection->has($columnAddress . $this->startRow)) && ($this->startRow <= $this->endRow) ) { ++$this->startRow; } while ( - (!$this->worksheet->cellExistsByColumnAndRow($this->columnIndex, $this->endRow)) && + (!$this->cellCollection->has($columnAddress . $this->endRow)) && ($this->endRow >= $this->startRow) ) { --$this->endRow; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php index 8a48f470c15..b64ecec9fea 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/ColumnDimension.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Helper\Dimension as CssDimension; class ColumnDimension extends Dimension @@ -53,22 +54,39 @@ class ColumnDimension extends Dimension /** * Set column index as string eg: 'A'. - * - * @return $this */ - public function setColumnIndex(string $index) + public function setColumnIndex(string $index): self { $this->columnIndex = $index; return $this; } + /** + * Get column index as numeric. + */ + public function getColumnNumeric(): int + { + return Coordinate::columnIndexFromString($this->columnIndex); + } + + /** + * Set column index as numeric. + */ + public function setColumnNumeric(int $index): self + { + $this->columnIndex = Coordinate::stringFromColumnIndex($index); + + return $this; + } + /** * Get Width. * - * Each unit of column width is equal to the width of one character in the default font size. - * By default, this will be the return value; but this method also accepts a unit of measure argument and will - * return the value converted to the specified UoM using an approximation method. + * Each unit of column width is equal to the width of one character in the default font size. A value of -1 + * tells Excel to display this column in its default width. + * By default, this will be the return value; but this method also accepts an optional unit of measure argument + * and will convert the returned value to the specified UoM.. */ public function getWidth(?string $unitOfMeasure = null): float { @@ -80,9 +98,11 @@ class ColumnDimension extends Dimension /** * Set Width. * - * Each unit of column width is equal to the width of one character in the default font size. - * By default, this will be the unit of measure for the passed value; but this method accepts a unit of measure - * argument, and will convert the value from the specified UoM using an approximation method. + * Each unit of column width is equal to the width of one character in the default font size. A value of -1 + * tells Excel to display this column in its default width. + * By default, this will be the unit of measure for the passed value; but this method also accepts an + * optional unit of measure argument, and will convert the value from the specified UoM using an + * approximation method. * * @return $this */ diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php index 894ac19ed49..ba03b5b72eb 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Dimension.php @@ -131,19 +131,4 @@ abstract class Dimension return $this; } - - /** - * Implement PHP __clone to create a deep clone, not just a shallow copy. - */ - public function __clone() - { - $vars = get_object_vars($this); - foreach ($vars as $key => $value) { - if (is_object($value)) { - $this->$key = clone $value; - } else { - $this->$key = $value; - } - } - } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php index f62873bbae4..ebda19b79c0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -106,7 +106,7 @@ class Drawing extends BaseDrawing */ public function setPath($path, $verifyFile = true, $zip = null) { - if ($verifyFile) { + if ($verifyFile && preg_match('~^data:image/[a-z]+;base64,~', $path) !== 1) { // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 if (filter_var($path, FILTER_VALIDATE_URL)) { $this->path = $path; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php index 91acbb7b8f4..b2d1aa694b7 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/MemoryDrawing.php @@ -47,6 +47,9 @@ class MemoryDrawing extends BaseDrawing */ private $uniqueName; + /** @var null|resource */ + private $alwaysNull; + /** * Create a new MemoryDrawing. */ @@ -56,6 +59,7 @@ class MemoryDrawing extends BaseDrawing $this->renderingFunction = self::RENDERING_DEFAULT; $this->mimeType = self::MIMETYPE_DEFAULT; $this->uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999)); + $this->alwaysNull = null; // Initialize parent parent::__construct(); @@ -64,8 +68,9 @@ class MemoryDrawing extends BaseDrawing public function __destruct() { if ($this->imageResource) { - imagedestroy($this->imageResource); - $this->imageResource = null; + $rslt = @imagedestroy($this->imageResource); + // "Fix" for Scrutinizer + $this->imageResource = $rslt ? null : $this->alwaysNull; } } @@ -81,8 +86,8 @@ class MemoryDrawing extends BaseDrawing return; } - $width = imagesx($this->imageResource); - $height = imagesy($this->imageResource); + $width = (int) imagesx($this->imageResource); + $height = (int) imagesy($this->imageResource); if (imageistruecolor($this->imageResource)) { $clone = imagecreatetruecolor($width, $height); @@ -145,8 +150,8 @@ class MemoryDrawing extends BaseDrawing if ($this->imageResource !== null) { // Get width/height - $this->width = imagesx($this->imageResource); - $this->height = imagesy($this->imageResource); + $this->width = (int) imagesx($this->imageResource); + $this->height = (int) imagesy($this->imageResource); } return $this; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php index 9640782877e..4bdc2d4cae8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/PageSetup.php @@ -259,10 +259,11 @@ class PageSetup /** * First page number. * - * @var int + * @var ?int */ private $firstPageNumber; + /** @var string */ private $pageOrder = self::PAGEORDER_DOWN_THEN_OVER; /** @@ -375,7 +376,7 @@ class PageSetup { // Microsoft Office Excel 2007 only allows setting a scale between 10 and 400 via the user interface, // but it is apparently still able to handle any scale >= 0, where 0 results in 100 - if (($scale >= 0) || $scale === null) { + if ($scale === null || $scale >= 0) { $this->scale = $scale; if ($update) { $this->fitToPage = false; @@ -640,6 +641,7 @@ class PageSetup if ($index == 0) { return $this->printArea; } + /** @phpstan-ignore-next-line */ $printAreas = explode(',', $this->printArea); if (isset($printAreas[$index - 1])) { return $printAreas[$index - 1]; @@ -663,6 +665,7 @@ class PageSetup if ($index == 0) { return $this->printArea !== null; } + /** @phpstan-ignore-next-line */ $printAreas = explode(',', $this->printArea); return isset($printAreas[$index - 1]); @@ -683,6 +686,7 @@ class PageSetup if ($index == 0) { $this->printArea = null; } else { + /** @phpstan-ignore-next-line */ $printAreas = explode(',', $this->printArea); if (isset($printAreas[$index - 1])) { unset($printAreas[$index - 1]); @@ -731,6 +735,7 @@ class PageSetup if ($index == 0) { $this->printArea = $value; } else { + /** @phpstan-ignore-next-line */ $printAreas = explode(',', $this->printArea); if ($index < 0) { $index = count($printAreas) - abs($index) + 1; @@ -745,6 +750,7 @@ class PageSetup if ($index == 0) { $this->printArea = $this->printArea ? ($this->printArea . ',' . $value) : $value; } else { + /** @phpstan-ignore-next-line */ $printAreas = explode(',', $this->printArea); if ($index < 0) { $index = abs($index) - 1; @@ -840,7 +846,7 @@ class PageSetup /** * Get first page number. * - * @return int + * @return ?int */ public function getFirstPageNumber() { @@ -850,7 +856,7 @@ class PageSetup /** * Set first page number. * - * @param int $value + * @param ?int $value * * @return $this */ diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php index b59333568f5..5c162752694 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Row.php @@ -23,7 +23,7 @@ class Row * * @param int $rowIndex */ - public function __construct(?Worksheet $worksheet = null, $rowIndex = 1) + public function __construct(Worksheet $worksheet, $rowIndex = 1) { // Set parent and row index $this->worksheet = $worksheet; @@ -35,8 +35,7 @@ class Row */ public function __destruct() { - // @phpstan-ignore-next-line - $this->worksheet = null; + $this->worksheet = null; // @phpstan-ignore-line } /** @@ -60,6 +59,45 @@ class Row return new RowCellIterator($this->worksheet, $this->rowIndex, $startColumn, $endColumn); } + /** + * Returns a boolean true if the row contains no cells. By default, this means that no cell records exist in the + * collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmpty(int $definitionOfEmptyFlags = 0): bool + { + $nullValueCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL); + $emptyStringCellIsEmpty = (bool) ($definitionOfEmptyFlags & CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL); + + $cellIterator = $this->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $cell) { + $value = $cell->getValue(); + if ($value === null && $nullValueCellIsEmpty === true) { + continue; + } + if ($value === '' && $emptyStringCellIsEmpty === true) { + continue; + } + + return false; + } + + return true; + } + /** * Returns bound worksheet. */ diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php index a78765bd328..5af05072e5a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowCellIterator.php @@ -51,6 +51,7 @@ class RowCellIterator extends CellIterator { // Set subject and row index $this->worksheet = $worksheet; + $this->cellCollection = $worksheet->getCellCollection(); $this->rowIndex = $rowIndex; $this->resetEnd($endColumn); $this->resetStart($startColumn); @@ -97,15 +98,14 @@ class RowCellIterator extends CellIterator */ public function seek(string $column = 'A') { - $columnx = $column; - $column = Coordinate::columnIndexFromString($column); - if ($this->onlyExistingCells && !($this->worksheet->cellExistsByColumnAndRow($column, $this->rowIndex))) { + $columnId = Coordinate::columnIndexFromString($column); + if ($this->onlyExistingCells && !($this->cellCollection->has($column . $this->rowIndex))) { throw new PhpSpreadsheetException('In "IterateOnlyExistingCells" mode and Cell does not exist'); } - if (($column < $this->startColumnIndex) || ($column > $this->endColumnIndex)) { - throw new PhpSpreadsheetException("Column $columnx is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"); + if (($columnId < $this->startColumnIndex) || ($columnId > $this->endColumnIndex)) { + throw new PhpSpreadsheetException("Column $column is out of range ({$this->startColumnIndex} - {$this->endColumnIndex})"); } - $this->currentColumnIndex = $column; + $this->currentColumnIndex = $columnId; return $this; } @@ -123,7 +123,11 @@ class RowCellIterator extends CellIterator */ public function current(): ?Cell { - return $this->worksheet->getCellByColumnAndRow($this->currentColumnIndex, $this->rowIndex); + $cellAddress = Coordinate::stringFromColumnIndex($this->currentColumnIndex) . $this->rowIndex; + + return $this->cellCollection->has($cellAddress) + ? $this->cellCollection->get($cellAddress) + : $this->worksheet->createNewCell($cellAddress); } /** @@ -141,7 +145,7 @@ class RowCellIterator extends CellIterator { do { ++$this->currentColumnIndex; - } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex <= $this->endColumnIndex)); + } while (($this->onlyExistingCells) && (!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->currentColumnIndex) . $this->rowIndex)) && ($this->currentColumnIndex <= $this->endColumnIndex)); } /** @@ -151,7 +155,7 @@ class RowCellIterator extends CellIterator { do { --$this->currentColumnIndex; - } while (($this->onlyExistingCells) && (!$this->worksheet->cellExistsByColumnAndRow($this->currentColumnIndex, $this->rowIndex)) && ($this->currentColumnIndex >= $this->startColumnIndex)); + } while (($this->onlyExistingCells) && (!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->currentColumnIndex) . $this->rowIndex)) && ($this->currentColumnIndex >= $this->startColumnIndex)); } /** @@ -176,10 +180,10 @@ class RowCellIterator extends CellIterator protected function adjustForExistingOnlyRange(): void { if ($this->onlyExistingCells) { - while ((!$this->worksheet->cellExistsByColumnAndRow($this->startColumnIndex, $this->rowIndex)) && ($this->startColumnIndex <= $this->endColumnIndex)) { + while ((!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->startColumnIndex) . $this->rowIndex)) && ($this->startColumnIndex <= $this->endColumnIndex)) { ++$this->startColumnIndex; } - while ((!$this->worksheet->cellExistsByColumnAndRow($this->endColumnIndex, $this->rowIndex)) && ($this->endColumnIndex >= $this->startColumnIndex)) { + while ((!$this->cellCollection->has(Coordinate::stringFromColumnIndex($this->endColumnIndex) . $this->rowIndex)) && ($this->endColumnIndex >= $this->startColumnIndex)) { --$this->endColumnIndex; } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php index 1d8aada85d1..acaafac55d7 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/RowDimension.php @@ -65,8 +65,9 @@ class RowDimension extends Dimension /** * Get Row Height. - * By default, this will be in points; but this method accepts a unit of measure - * argument, and will convert the value to the specified UoM. + * By default, this will be in points; but this method also accepts an optional unit of measure + * argument, and will convert the value from points to the specified UoM. + * A value of -1 tells Excel to display this column in its default height. * * @return float */ @@ -80,8 +81,8 @@ class RowDimension extends Dimension /** * Set Row Height. * - * @param float $height in points - * By default, this will be the passed argument value; but this method accepts a unit of measure + * @param float $height in points. A value of -1 tells Excel to display this column in its default height. + * By default, this will be the passed argument value; but this method also accepts an optional unit of measure * argument, and will convert the passed argument value to points from the specified UoM * * @return $this diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php new file mode 100644 index 00000000000..ffdbf9a780e --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table.php @@ -0,0 +1,454 @@ +|string $range + * A simple string containing a Cell range like 'A1:E10' is permitted + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * @param string $name (e.g. Table1) + */ + public function __construct($range = '', string $name = '') + { + $this->setRange($range); + $this->setName($name); + $this->style = new TableStyle(); + } + + /** + * Get Table name. + */ + public function getName(): string + { + return $this->name; + } + + /** + * Set Table name. + */ + public function setName(string $name): self + { + $name = trim($name); + + if (!empty($name)) { + if (strlen($name) === 1 && in_array($name, ['C', 'c', 'R', 'r'])) { + throw new PhpSpreadsheetException('The table name is invalid'); + } + if (strlen($name) > 255) { + throw new PhpSpreadsheetException('The table name cannot be longer than 255 characters'); + } + // Check for A1 or R1C1 cell reference notation + if ( + preg_match(Coordinate::A1_COORDINATE_REGEX, $name) || + preg_match('/^R\[?\-?[0-9]*\]?C\[?\-?[0-9]*\]?$/i', $name) + ) { + throw new PhpSpreadsheetException('The table name can\'t be the same as a cell reference'); + } + if (!preg_match('/^[\p{L}_\\\\]/iu', $name)) { + throw new PhpSpreadsheetException('The table name must begin a name with a letter, an underscore character (_), or a backslash (\)'); + } + if (!preg_match('/^[\p{L}_\\\\][\p{L}\p{M}0-9\._]+$/iu', $name)) { + throw new PhpSpreadsheetException('The table name contains invalid characters'); + } + } + + $this->name = $name; + + return $this; + } + + /** + * Get show Header Row. + */ + public function getShowHeaderRow(): bool + { + return $this->showHeaderRow; + } + + /** + * Set show Header Row. + */ + public function setShowHeaderRow(bool $showHeaderRow): self + { + $this->showHeaderRow = $showHeaderRow; + + return $this; + } + + /** + * Get show Totals Row. + */ + public function getShowTotalsRow(): bool + { + return $this->showTotalsRow; + } + + /** + * Set show Totals Row. + */ + public function setShowTotalsRow(bool $showTotalsRow): self + { + $this->showTotalsRow = $showTotalsRow; + + return $this; + } + + /** + * Get Table Range. + */ + public function getRange(): string + { + return $this->range; + } + + /** + * Set Table Cell Range. + * + * @param AddressRange|array|string $range + * A simple string containing a Cell range like 'A1:E10' is permitted + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + */ + public function setRange($range = ''): self + { + // extract coordinate + if ($range !== '') { + [, $range] = Worksheet::extractSheetTitle(Validations::validateCellRange($range), true); + } + if (empty($range)) { + // Discard all column rules + $this->columns = []; + $this->range = ''; + + return $this; + } + + if (strpos($range, ':') === false) { + throw new PhpSpreadsheetException('Table must be set on a range of cells.'); + } + + [$width, $height] = Coordinate::rangeDimension($range); + if ($width < 1 || $height < 2) { + throw new PhpSpreadsheetException('The table range must be at least 1 column and 2 rows'); + } + + $this->range = $range; + // Discard any column ruless that are no longer valid within this range + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); + foreach ($this->columns as $key => $value) { + $colIndex = Coordinate::columnIndexFromString($key); + if (($rangeStart[0] > $colIndex) || ($rangeEnd[0] < $colIndex)) { + unset($this->columns[$key]); + } + } + + return $this; + } + + /** + * Set Table Cell Range to max row. + */ + public function setRangeToMaxRow(): self + { + if ($this->workSheet !== null) { + $thisrange = $this->range; + $range = (string) preg_replace('/\\d+$/', (string) $this->workSheet->getHighestRow(), $thisrange); + if ($range !== $thisrange) { + $this->setRange($range); + } + } + + return $this; + } + + /** + * Get Table's Worksheet. + */ + public function getWorksheet(): ?Worksheet + { + return $this->workSheet; + } + + /** + * Set Table's Worksheet. + */ + public function setWorksheet(?Worksheet $worksheet = null): self + { + if ($this->name !== '' && $worksheet !== null) { + $spreadsheet = $worksheet->getParent(); + $tableName = StringHelper::strToUpper($this->name); + + foreach ($spreadsheet->getWorksheetIterator() as $sheet) { + foreach ($sheet->getTableCollection() as $table) { + if (StringHelper::strToUpper($table->getName()) === $tableName) { + throw new PhpSpreadsheetException("Workbook already contains a table named '{$this->name}'"); + } + } + } + } + + $this->workSheet = $worksheet; + + return $this; + } + + /** + * Get all Table Columns. + * + * @return Table\Column[] + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * Validate that the specified column is in the Table range. + * + * @param string $column Column name (e.g. A) + * + * @return int The column offset within the table range + */ + public function isColumnInRange(string $column): int + { + if (empty($this->range)) { + throw new PhpSpreadsheetException('No table range is defined.'); + } + + $columnIndex = Coordinate::columnIndexFromString($column); + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); + if (($rangeStart[0] > $columnIndex) || ($rangeEnd[0] < $columnIndex)) { + throw new PhpSpreadsheetException('Column is outside of current table range.'); + } + + return $columnIndex - $rangeStart[0]; + } + + /** + * Get a specified Table Column Offset within the defined Table range. + * + * @param string $column Column name (e.g. A) + * + * @return int The offset of the specified column within the table range + */ + public function getColumnOffset($column): int + { + return $this->isColumnInRange($column); + } + + /** + * Get a specified Table Column. + * + * @param string $column Column name (e.g. A) + */ + public function getColumn($column): Table\Column + { + $this->isColumnInRange($column); + + if (!isset($this->columns[$column])) { + $this->columns[$column] = new Table\Column($column, $this); + } + + return $this->columns[$column]; + } + + /** + * Get a specified Table Column by it's offset. + * + * @param int $columnOffset Column offset within range (starting from 0) + */ + public function getColumnByOffset($columnOffset): Table\Column + { + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($this->range); + $pColumn = Coordinate::stringFromColumnIndex($rangeStart[0] + $columnOffset); + + return $this->getColumn($pColumn); + } + + /** + * Set Table. + * + * @param string|Table\Column $columnObjectOrString + * A simple string containing a Column ID like 'A' is permitted + */ + public function setColumn($columnObjectOrString): self + { + if ((is_string($columnObjectOrString)) && (!empty($columnObjectOrString))) { + $column = $columnObjectOrString; + } elseif (is_object($columnObjectOrString) && ($columnObjectOrString instanceof Table\Column)) { + $column = $columnObjectOrString->getColumnIndex(); + } else { + throw new PhpSpreadsheetException('Column is not within the table range.'); + } + $this->isColumnInRange($column); + + if (is_string($columnObjectOrString)) { + $this->columns[$columnObjectOrString] = new Table\Column($columnObjectOrString, $this); + } else { + $columnObjectOrString->setTable($this); + $this->columns[$column] = $columnObjectOrString; + } + ksort($this->columns); + + return $this; + } + + /** + * Clear a specified Table Column. + * + * @param string $column Column name (e.g. A) + */ + public function clearColumn($column): self + { + $this->isColumnInRange($column); + + if (isset($this->columns[$column])) { + unset($this->columns[$column]); + } + + return $this; + } + + /** + * Shift an Table Column Rule to a different column. + * + * Note: This method bypasses validation of the destination column to ensure it is within this Table range. + * Nor does it verify whether any column rule already exists at $toColumn, but will simply override any existing value. + * Use with caution. + * + * @param string $fromColumn Column name (e.g. A) + * @param string $toColumn Column name (e.g. B) + */ + public function shiftColumn($fromColumn, $toColumn): self + { + $fromColumn = strtoupper($fromColumn); + $toColumn = strtoupper($toColumn); + + if (($fromColumn !== null) && (isset($this->columns[$fromColumn])) && ($toColumn !== null)) { + $this->columns[$fromColumn]->setTable(); + $this->columns[$fromColumn]->setColumnIndex($toColumn); + $this->columns[$toColumn] = $this->columns[$fromColumn]; + $this->columns[$toColumn]->setTable($this); + unset($this->columns[$fromColumn]); + + ksort($this->columns); + } + + return $this; + } + + /** + * Get table Style. + */ + public function getStyle(): Table\TableStyle + { + return $this->style; + } + + /** + * Set table Style. + */ + public function setStyle(TableStyle $style): self + { + $this->style = $style; + + return $this; + } + + /** + * Implement PHP __clone to create a deep clone, not just a shallow copy. + */ + public function __clone() + { + $vars = get_object_vars($this); + foreach ($vars as $key => $value) { + if (is_object($value)) { + if ($key === 'workSheet') { + // Detach from worksheet + $this->{$key} = null; + } else { + $this->{$key} = clone $value; + } + } elseif ((is_array($value)) && ($key === 'columns')) { + // The columns array of \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\Table objects + $this->{$key} = []; + foreach ($value as $k => $v) { + $this->{$key}[$k] = clone $v; + // attach the new cloned Column to this new cloned Table object + $this->{$key}[$k]->setTable($this); + } + } else { + $this->{$key} = $value; + } + } + } + + /** + * toString method replicates previous behavior by returning the range if object is + * referenced as a property of its worksheet. + */ + public function __toString() + { + return (string) $this->range; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/Column.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/Column.php new file mode 100644 index 00000000000..a7c445f5307 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/Column.php @@ -0,0 +1,203 @@ +columnIndex = $column; + $this->table = $table; + } + + /** + * Get Table column index as string eg: 'A'. + */ + public function getColumnIndex(): string + { + return $this->columnIndex; + } + + /** + * Set Table column index as string eg: 'A'. + * + * @param string $column Column (e.g. A) + */ + public function setColumnIndex($column): self + { + // Uppercase coordinate + $column = strtoupper($column); + if ($this->table !== null) { + $this->table->isColumnInRange($column); + } + + $this->columnIndex = $column; + + return $this; + } + + /** + * Get show Filter Button. + */ + public function getShowFilterButton(): bool + { + return $this->showFilterButton; + } + + /** + * Set show Filter Button. + */ + public function setShowFilterButton(bool $showFilterButton): self + { + $this->showFilterButton = $showFilterButton; + + return $this; + } + + /** + * Get total Row Label. + */ + public function getTotalsRowLabel(): ?string + { + return $this->totalsRowLabel; + } + + /** + * Set total Row Label. + */ + public function setTotalsRowLabel(string $totalsRowLabel): self + { + $this->totalsRowLabel = $totalsRowLabel; + + return $this; + } + + /** + * Get total Row Function. + */ + public function getTotalsRowFunction(): ?string + { + return $this->totalsRowFunction; + } + + /** + * Set total Row Function. + */ + public function setTotalsRowFunction(string $totalsRowFunction): self + { + $this->totalsRowFunction = $totalsRowFunction; + + return $this; + } + + /** + * Get total Row Formula. + */ + public function getTotalsRowFormula(): ?string + { + return $this->totalsRowFormula; + } + + /** + * Set total Row Formula. + */ + public function setTotalsRowFormula(string $totalsRowFormula): self + { + $this->totalsRowFormula = $totalsRowFormula; + + return $this; + } + + /** + * Get column Formula. + */ + public function getColumnFormula(): ?string + { + return $this->columnFormula; + } + + /** + * Set column Formula. + */ + public function setColumnFormula(string $columnFormula): self + { + $this->columnFormula = $columnFormula; + + return $this; + } + + /** + * Get this Column's Table. + */ + public function getTable(): ?Table + { + return $this->table; + } + + /** + * Set this Column's Table. + */ + public function setTable(?Table $table = null): self + { + $this->table = $table; + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php new file mode 100644 index 00000000000..78643c729e1 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Table/TableStyle.php @@ -0,0 +1,230 @@ +theme = $theme; + } + + /** + * Get theme. + */ + public function getTheme(): string + { + return $this->theme; + } + + /** + * Set theme. + */ + public function setTheme(string $theme): self + { + $this->theme = $theme; + + return $this; + } + + /** + * Get show First Column. + */ + public function getShowFirstColumn(): bool + { + return $this->showFirstColumn; + } + + /** + * Set show First Column. + */ + public function setShowFirstColumn(bool $showFirstColumn): self + { + $this->showFirstColumn = $showFirstColumn; + + return $this; + } + + /** + * Get show Last Column. + */ + public function getShowLastColumn(): bool + { + return $this->showLastColumn; + } + + /** + * Set show Last Column. + */ + public function setShowLastColumn(bool $showLastColumn): self + { + $this->showLastColumn = $showLastColumn; + + return $this; + } + + /** + * Get show Row Stripes. + */ + public function getShowRowStripes(): bool + { + return $this->showRowStripes; + } + + /** + * Set show Row Stripes. + */ + public function setShowRowStripes(bool $showRowStripes): self + { + $this->showRowStripes = $showRowStripes; + + return $this; + } + + /** + * Get show Column Stripes. + */ + public function getShowColumnStripes(): bool + { + return $this->showColumnStripes; + } + + /** + * Set show Column Stripes. + */ + public function setShowColumnStripes(bool $showColumnStripes): self + { + $this->showColumnStripes = $showColumnStripes; + + return $this; + } + + /** + * Get this Style's Table. + */ + public function getTable(): ?Table + { + return $this->table; + } + + /** + * Set this Style's Table. + */ + public function setTable(?Table $table = null): self + { + $this->table = $table; + + return $this; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php new file mode 100644 index 00000000000..a56dda4c778 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Validations.php @@ -0,0 +1,101 @@ +|CellAddress|string $cellAddress Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + */ + public static function validateCellAddress($cellAddress): string + { + if (is_string($cellAddress)) { + [$worksheet, $address] = Worksheet::extractSheetTitle($cellAddress, true); +// if (!empty($worksheet) && $worksheet !== $this->getTitle()) { +// throw new Exception('Reference is not for this worksheet'); +// } + + return empty($worksheet) ? strtoupper($address) : $worksheet . '!' . strtoupper($address); + } + + if (is_array($cellAddress)) { + $cellAddress = CellAddress::fromColumnRowArray($cellAddress); + } + + return (string) $cellAddress; + } + + /** + * Validate a cell address or cell range. + * + * @param AddressRange|array|CellAddress|int|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as a CellAddress or AddressRange object. + */ + public static function validateCellOrCellRange($cellRange): string + { + if (is_string($cellRange) || is_numeric($cellRange)) { + // Convert a single column reference like 'A' to 'A:A', + // a single row reference like '1' to '1:1' + $cellRange = (string) preg_replace('/^([A-Z]+|\d+)$/', '${1}:${1}', (string) $cellRange); + } elseif (is_object($cellRange) && $cellRange instanceof CellAddress) { + $cellRange = new CellRange($cellRange, $cellRange); + } + + return self::validateCellRange($cellRange); + } + + /** + * Validate a cell range. + * + * @param AddressRange|array|string $cellRange Coordinate of the cells as a string, eg: 'C5:F12'; + * or as an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 12]), + * or as an AddressRange object. + */ + public static function validateCellRange($cellRange): string + { + if (is_string($cellRange)) { + [$worksheet, $addressRange] = Worksheet::extractSheetTitle($cellRange, true); + + // Convert Column ranges like 'A:C' to 'A1:C1048576' + // or Row ranges like '1:3' to 'A1:XFD3' + $addressRange = (string) preg_replace( + ['/^([A-Z]+):([A-Z]+)$/i', '/^(\\d+):(\\d+)$/'], + ['${1}1:${2}1048576', 'A${1}:XFD${2}'], + $addressRange + ); + + return empty($worksheet) ? strtoupper($addressRange) : $worksheet . '!' . strtoupper($addressRange); + } + + if (is_array($cellRange)) { + [$from, $to] = array_chunk($cellRange, 2); + $cellRange = new CellRange(CellAddress::fromColumnRowArray($from), CellAddress::fromColumnRowArray($to)); + } + + return (string) $cellRange; + } + + public static function definedNameToCoordinate(string $coordinate, Worksheet $worksheet): string + { + // Uppercase coordinate + $coordinate = strtoupper($coordinate); + // Eliminate leading equal sign + $testCoordinate = (string) preg_replace('/^=/', '', $coordinate); + $defined = $worksheet->getParent()->getDefinedName($testCoordinate, $worksheet); + if ($defined !== null) { + if ($defined->getWorksheet() === $worksheet && !$defined->isFormula()) { + $coordinate = (string) preg_replace('/^=/', '', $defined->getValue()); + } + } + + return $coordinate; + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php index ca8c3dfe429..8235ccf1403 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -4,7 +4,11 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; use ArrayObject; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Cell\CellAddress; +use PhpOffice\PhpSpreadsheet\Cell\CellRange; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DataValidation; @@ -28,14 +32,20 @@ use PhpOffice\PhpSpreadsheet\Style\Style; class Worksheet implements IComparable { // Break types - const BREAK_NONE = 0; - const BREAK_ROW = 1; - const BREAK_COLUMN = 2; + public const BREAK_NONE = 0; + public const BREAK_ROW = 1; + public const BREAK_COLUMN = 2; // Sheet state - const SHEETSTATE_VISIBLE = 'visible'; - const SHEETSTATE_HIDDEN = 'hidden'; - const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + public const SHEETSTATE_VISIBLE = 'visible'; + public const SHEETSTATE_HIDDEN = 'hidden'; + public const SHEETSTATE_VERYHIDDEN = 'veryHidden'; + + public const MERGE_CELL_CONTENT_EMPTY = 'empty'; + public const MERGE_CELL_CONTENT_HIDE = 'hide'; + public const MERGE_CELL_CONTENT_MERGE = 'merge'; + + protected const SHEET_NAME_REQUIRES_NO_QUOTES = '/^[_\p{L}][_\p{L}\p{N}]*$/mui'; /** * Maximum 31 characters allowed for sheet title. @@ -107,6 +117,13 @@ class Worksheet implements IComparable */ private $chartCollection; + /** + * Collection of Table objects. + * + * @var ArrayObject + */ + private $tableCollection; + /** * Worksheet title. * @@ -170,13 +187,6 @@ class Worksheet implements IComparable */ private $conditionalStylesCollection = []; - /** - * Is the current cell collection sorted already? - * - * @var bool - */ - private $cellCollectionIsSorted = false; - /** * Collection of breaks. * @@ -371,7 +381,10 @@ class Worksheet implements IComparable $this->defaultRowDimension = new RowDimension(null); // Default column dimension $this->defaultColumnDimension = new ColumnDimension(null); + // AutoFilter $this->autoFilter = new AutoFilter('', $this); + // Table collection + $this->tableCollection = new ArrayObject(); } /** @@ -567,6 +580,7 @@ class Worksheet implements IComparable $this->chartCollection[] = $chart; } else { // Insert the chart at the requested index + // @phpstan-ignore-next-line array_splice($this->chartCollection, $chartIndex, 0, [$chart]); } @@ -650,10 +664,8 @@ class Worksheet implements IComparable */ public function refreshColumnDimensions() { - $currentColumnDimensions = $this->getColumnDimensions(); $newColumnDimensions = []; - - foreach ($currentColumnDimensions as $objColumnDimension) { + foreach ($this->getColumnDimensions() as $objColumnDimension) { $newColumnDimensions[$objColumnDimension->getColumnIndex()] = $objColumnDimension; } @@ -669,10 +681,8 @@ class Worksheet implements IComparable */ public function refreshRowDimensions() { - $currentRowDimensions = $this->getRowDimensions(); $newRowDimensions = []; - - foreach ($currentRowDimensions as $objRowDimension) { + foreach ($this->getRowDimensions() as $objRowDimension) { $newRowDimensions[$objRowDimension->getRowIndex()] = $objRowDimension; } @@ -728,9 +738,19 @@ class Worksheet implements IComparable } } + $autoFilterRange = $autoFilterFirstRowRange = $this->autoFilter->getRange(); + if (!empty($autoFilterRange)) { + $autoFilterRangeBoundaries = Coordinate::rangeBoundaries($autoFilterRange); + $autoFilterFirstRowRange = (string) new CellRange( + CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[0][0], $autoFilterRangeBoundaries[0][1]), + CellAddress::fromColumnAndRow($autoFilterRangeBoundaries[1][0], $autoFilterRangeBoundaries[0][1]) + ); + } + // loop through all cells in the worksheet foreach ($this->getCoordinates(false) as $coordinate) { $cell = $this->getCellOrNull($coordinate); + if ($cell !== null && isset($autoSizes[$this->cellCollection->getCurrentColumn()])) { //Determine if cell is in merge range $isMerged = isset($isMergeCell[$this->cellCollection->getCurrentCoordinate()]); @@ -738,22 +758,32 @@ class Worksheet implements IComparable //By default merged cells should be ignored $isMergedButProceed = false; - //The only exception is if it's a merge range value cell of a 'vertical' randge (1 column wide) + //The only exception is if it's a merge range value cell of a 'vertical' range (1 column wide) if ($isMerged && $cell->isMergeRangeValueCell()) { $range = $cell->getMergeRange(); $rangeBoundaries = Coordinate::rangeDimension($range); - if ($rangeBoundaries[0] == 1) { + if ($rangeBoundaries[0] === 1) { $isMergedButProceed = true; } } - // Determine width if cell does not participate in a merge or does and is a value cell of 1-column wide range + // Determine width if cell is not part of a merge or does and is a value cell of 1-column wide range if (!$isMerged || $isMergedButProceed) { + // Determine if we need to make an adjustment for the first row in an AutoFilter range that + // has a column filter dropdown + $filterAdjustment = false; + if (!empty($autoFilterRange) && $cell->isInRange($autoFilterFirstRowRange)) { + $filterAdjustment = true; + } + + $indentAdjustment = $cell->getStyle()->getAlignment()->getIndent(); + // Calculated value // To formatted string $cellValue = NumberFormat::toFormattedString( $cell->getCalculatedValue(), - $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode() + $this->getParent()->getCellXfByIndex($cell->getXfIndex()) + ->getNumberFormat()->getFormatCode() ); if ($cellValue !== null && $cellValue !== '') { @@ -762,8 +792,11 @@ class Worksheet implements IComparable (float) Shared\Font::calculateColumnWidth( $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont(), $cellValue, - $this->getParent()->getCellXfByIndex($cell->getXfIndex())->getAlignment()->getTextRotation(), - $this->getParent()->getDefaultStyle()->getFont() + $this->getParent()->getCellXfByIndex($cell->getXfIndex()) + ->getAlignment()->getTextRotation(), + $this->getParent()->getDefaultStyle()->getFont(), + $filterAdjustment, + $indentAdjustment ) ); } @@ -890,7 +923,7 @@ class Worksheet implements IComparable $this->parent->getCalculationEngine() ->renameCalculationCacheForWorksheet($oldTitle, $newTitle); if ($updateFormulaCellReferences) { - ReferenceHelper::getInstance()->updateNamedFormulas($this->parent, $oldTitle, $newTitle); + ReferenceHelper::getInstance()->updateNamedFormulae($this->parent, $oldTitle, $newTitle); } } @@ -1042,7 +1075,7 @@ class Worksheet implements IComparable */ public function getHighestColumn($row = null) { - if (empty($row)) { + if ($row === null) { return Coordinate::stringFromColumnIndex($this->cachedHighestColumn); } @@ -1072,7 +1105,7 @@ class Worksheet implements IComparable */ public function getHighestRow($column = null) { - if ($column == null) { + if ($column === null) { return $this->cachedHighestRow; } @@ -1105,14 +1138,16 @@ class Worksheet implements IComparable /** * Set a cell value. * - * @param string $coordinate Coordinate of the cell, eg: 'A1' - * @param mixed $value Value of the cell + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * @param mixed $value Value for the cell * * @return $this */ public function setCellValue($coordinate, $value) { - $this->getCell($coordinate)->setValue($value); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); + $this->getCell($cellAddress)->setValue($value); return $this; } @@ -1120,6 +1155,10 @@ class Worksheet implements IComparable /** * Set a cell value by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setCellValue() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @param mixed $value Value of the cell @@ -1128,7 +1167,7 @@ class Worksheet implements IComparable */ public function setCellValueByColumnAndRow($columnIndex, $row, $value) { - $this->getCellByColumnAndRow($columnIndex, $row)->setValue($value); + $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValue($value); return $this; } @@ -1136,16 +1175,22 @@ class Worksheet implements IComparable /** * Set a cell value. * - * @param string $coordinate Coordinate of the cell, eg: 'A1' + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * @param mixed $value Value of the cell * @param string $dataType Explicit data type, see DataType::TYPE_* + * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this + * method, then it is your responsibility as an end-user developer to validate that the value and + * the datatype match. + * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype + * that you specify. * * @return $this */ public function setCellValueExplicit($coordinate, $value, $dataType) { - // Set value - $this->getCell($coordinate)->setValueExplicit($value, $dataType); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); + $this->getCell($cellAddress)->setValueExplicit($value, $dataType); return $this; } @@ -1153,16 +1198,25 @@ class Worksheet implements IComparable /** * Set a cell value by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setCellValueExplicit() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @param mixed $value Value of the cell * @param string $dataType Explicit data type, see DataType::TYPE_* + * Note that PhpSpreadsheet does not validate that the value and datatype are consistent, in using this + * method, then it is your responsibility as an end-user developer to validate that the value and + * the datatype match. + * If you do mismatch value and datatpe, then the value you enter may be changed to match the datatype + * that you specify. * * @return $this */ public function setCellValueExplicitByColumnAndRow($columnIndex, $row, $value, $dataType) { - $this->getCellByColumnAndRow($columnIndex, $row)->setValueExplicit($value, $dataType); + $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row)->setValueExplicit($value, $dataType); return $this; } @@ -1170,22 +1224,25 @@ class Worksheet implements IComparable /** * Get cell at a specific coordinate. * - * @param string $coordinate Coordinate of the cell, eg: 'A1' + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * * @return Cell Cell that was found or created */ - public function getCell(string $coordinate): Cell + public function getCell($coordinate): Cell { + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); + // Shortcut for increased performance for the vast majority of simple cases - if ($this->cellCollection->has($coordinate)) { + if ($this->cellCollection->has($cellAddress)) { /** @var Cell $cell */ - $cell = $this->cellCollection->get($coordinate); + $cell = $this->cellCollection->get($cellAddress); return $cell; } /** @var Worksheet $sheet */ - [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate); + [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); $cell = $sheet->cellCollection->get($finalCoordinate); return $cell ?? $sheet->createNewCell($finalCoordinate); @@ -1209,27 +1266,28 @@ class Worksheet implements IComparable $sheet = $this->parent->getSheetByName($worksheetReference[0]); $finalCoordinate = strtoupper($worksheetReference[1]); - if (!$sheet) { + if ($sheet === null) { throw new Exception('Sheet not found for name: ' . $worksheetReference[0]); } } elseif ( !preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $coordinate) && - preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $coordinate) + preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/iu', $coordinate) ) { // Named range? $namedRange = $this->validateNamedRange($coordinate, true); if ($namedRange !== null) { $sheet = $namedRange->getWorksheet(); - if (!$sheet) { + if ($sheet === null) { throw new Exception('Sheet not found for named range: ' . $namedRange->getName()); } + /** @phpstan-ignore-next-line */ $cellCoordinate = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!'); $finalCoordinate = str_replace('$', '', $cellCoordinate); } } - if (!$sheet || !$finalCoordinate) { + if ($sheet === null || $finalCoordinate === null) { $sheet = $this; $finalCoordinate = strtoupper($coordinate); } @@ -1263,6 +1321,10 @@ class Worksheet implements IComparable /** * Get cell at a specific coordinate by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the getCell() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @@ -1270,18 +1332,7 @@ class Worksheet implements IComparable */ public function getCellByColumnAndRow($columnIndex, $row): Cell { - $columnLetter = Coordinate::stringFromColumnIndex($columnIndex); - $coordinate = $columnLetter . $row; - - if ($this->cellCollection->has($coordinate)) { - /** @var Cell $cell */ - $cell = $this->cellCollection->get($coordinate); - - return $cell; - } - - // Create new cell object, if required - return $this->createNewCell($coordinate); + return $this->getCell(Coordinate::stringFromColumnIndex($columnIndex) . $row); } /** @@ -1291,30 +1342,28 @@ class Worksheet implements IComparable * * @return Cell Cell that was created */ - private function createNewCell($coordinate) + public function createNewCell($coordinate): Cell { + [$column, $row, $columnString] = Coordinate::indexesFromString($coordinate); $cell = new Cell(null, DataType::TYPE_NULL, $this); $this->cellCollection->add($coordinate, $cell); - $this->cellCollectionIsSorted = false; // Coordinates - [$column, $row] = Coordinate::coordinateFromString($coordinate); - $aIndexes = Coordinate::indexesFromString($coordinate); - if ($this->cachedHighestColumn < $aIndexes[0]) { - $this->cachedHighestColumn = $aIndexes[0]; + if ($column > $this->cachedHighestColumn) { + $this->cachedHighestColumn = $column; } - if ($aIndexes[1] > $this->cachedHighestRow) { - $this->cachedHighestRow = $aIndexes[1]; + if ($row > $this->cachedHighestRow) { + $this->cachedHighestRow = $row; } // Cell needs appropriate xfIndex from dimensions records // but don't create dimension records if they don't already exist $rowDimension = $this->rowDimensions[$row] ?? null; - $columnDimension = $this->columnDimensions[$column] ?? null; + $columnDimension = $this->columnDimensions[$columnString] ?? null; if ($rowDimension !== null && $rowDimension->getXfIndex() > 0) { // then there is a row dimension with explicit style, assign it to the cell - $cell->setXfIndex($rowDimension->getXfIndex()); + $cell->setXfIndex(/** @scrutinizer ignore-type */ $rowDimension->getXfIndex()); } elseif ($columnDimension !== null && $columnDimension->getXfIndex() > 0) { // then there is a column dimension, assign it to the cell $cell->setXfIndex($columnDimension->getXfIndex()); @@ -1326,14 +1375,14 @@ class Worksheet implements IComparable /** * Does the cell at a specific coordinate exist? * - * @param string $coordinate Coordinate of the cell eg: 'A1' - * - * @return bool + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. */ - public function cellExists($coordinate) + public function cellExists($coordinate): bool { + $cellAddress = Validations::validateCellAddress($coordinate); /** @var Worksheet $sheet */ - [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($coordinate); + [$sheet, $finalCoordinate] = $this->getWorksheetAndCoordinate($cellAddress); return $sheet->cellCollection->has($finalCoordinate); } @@ -1341,12 +1390,14 @@ class Worksheet implements IComparable /** * Cell at a specific coordinate by using numeric cell coordinates exists? * + * @Deprecated 1.23.0 + * Use the cellExists() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell - * - * @return bool */ - public function cellExistsByColumnAndRow($columnIndex, $row) + public function cellExistsByColumnAndRow($columnIndex, $row): bool { return $this->cellExists(Coordinate::stringFromColumnIndex($columnIndex) . $row); } @@ -1368,6 +1419,11 @@ class Worksheet implements IComparable return $this->rowDimensions[$row]; } + public function rowDimensionExists(int $row): bool + { + return isset($this->rowDimensions[$row]); + } + /** * Get column dimension at a specific column. * @@ -1414,12 +1470,15 @@ class Worksheet implements IComparable /** * Get style for cell. * - * @param string $cellCoordinate Cell coordinate (or range) to get style for, eg: 'A1' - * - * @return Style + * @param AddressRange|array|CellAddress|int|string $cellCoordinate + * A simple string containing a cell address like 'A1' or a cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. */ - public function getStyle($cellCoordinate) + public function getStyle($cellCoordinate): Style { + $cellCoordinate = Validations::validateCellOrCellRange($cellCoordinate); + // set this sheet as active $this->parent->setActiveSheetIndex($this->parent->getIndex($this)); @@ -1429,6 +1488,35 @@ class Worksheet implements IComparable return $this->parent->getCellXfSupervisor(); } + /** + * Get style for cell by using numeric cell coordinates. + * + * @Deprecated 1.23.0 + * Use the getStyle() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * + * @param int $columnIndex1 Numeric column coordinate of the cell + * @param int $row1 Numeric row coordinate of the cell + * @param null|int $columnIndex2 Numeric column coordinate of the range cell + * @param null|int $row2 Numeric row coordinate of the range cell + * + * @return Style + */ + public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null) + { + if ($columnIndex2 !== null && $row2 !== null) { + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); + + return $this->getStyle($cellRange); + } + + return $this->getStyle(CellAddress::fromColumnAndRow($columnIndex1, $row1)); + } + /** * Get conditional styles for a cell. * @@ -1440,7 +1528,7 @@ class Worksheet implements IComparable * * @return Conditional[] */ - public function getConditionalStyles($coordinate) + public function getConditionalStyles(string $coordinate): array { $coordinate = strtoupper($coordinate); if (strpos($coordinate, ':') !== false) { @@ -1457,6 +1545,19 @@ class Worksheet implements IComparable return []; } + public function getConditionalRange(string $coordinate): ?string + { + $coordinate = strtoupper($coordinate); + $cell = $this->getCell($coordinate); + foreach (array_keys($this->conditionalStylesCollection) as $conditionalRange) { + if ($cell->isInRange($conditionalRange)) { + return $conditionalRange; + } + } + + return null; + } + /** * Do conditional styles exist for this cell? * @@ -1465,14 +1566,12 @@ class Worksheet implements IComparable * conditional style range. * If a range of cells is specified, then true will only be returned if the range matches the entire * range of the conditional. - * - * @return bool */ - public function conditionalStylesExists($coordinate) + public function conditionalStylesExists($coordinate): bool { $coordinate = strtoupper($coordinate); if (strpos($coordinate, ':') !== false) { - return isset($this->conditionalStylesCollection[strtoupper($coordinate)]); + return isset($this->conditionalStylesCollection[$coordinate]); } $cell = $this->getCell($coordinate); @@ -1524,27 +1623,6 @@ class Worksheet implements IComparable return $this; } - /** - * Get style for cell by using numeric cell coordinates. - * - * @param int $columnIndex1 Numeric column coordinate of the cell - * @param int $row1 Numeric row coordinate of the cell - * @param null|int $columnIndex2 Numeric column coordinate of the range cell - * @param null|int $row2 Numeric row coordinate of the range cell - * - * @return Style - */ - public function getStyleByColumnAndRow($columnIndex1, $row1, $columnIndex2 = null, $row2 = null) - { - if ($columnIndex2 !== null && $row2 !== null) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; - - return $this->getStyle($cellRange); - } - - return $this->getStyle(Coordinate::stringFromColumnIndex($columnIndex1) . $row1); - } - /** * Duplicate cell style to a range of cells. * @@ -1629,26 +1707,22 @@ class Worksheet implements IComparable /** * Set break on a cell. * - * @param string $coordinate Cell coordinate (e.g. A1) + * @param array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. * @param int $break Break type (type of Worksheet::BREAK_*) * * @return $this */ public function setBreak($coordinate, $break) { - // Uppercase coordinate - $coordinate = strtoupper($coordinate); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)); - if ($coordinate != '') { - if ($break == self::BREAK_NONE) { - if (isset($this->breaks[$coordinate])) { - unset($this->breaks[$coordinate]); - } - } else { - $this->breaks[$coordinate] = $break; + if ($break === self::BREAK_NONE) { + if (isset($this->breaks[$cellAddress])) { + unset($this->breaks[$cellAddress]); } } else { - throw new Exception('No cell coordinate specified.'); + $this->breaks[$cellAddress] = $break; } return $this; @@ -1657,6 +1731,10 @@ class Worksheet implements IComparable /** * Set break on a cell by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setBreak() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @param int $break Break type (type of Worksheet::BREAK_*) @@ -1681,71 +1759,168 @@ class Worksheet implements IComparable /** * Set merge on a cell range. * - * @param string $range Cell range (e.g. A1:E1) + * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange. + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCells($range) + public function mergeCells($range, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { - // Uppercase coordinate - $range = strtoupper($range); + $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); - if (strpos($range, ':') !== false) { - $this->mergeCells[$range] = $range; + if (strpos($range, ':') === false) { + $range .= ":{$range}"; + } - // make sure cells are created + if (preg_match('/^([A-Z]+)(\\d+):([A-Z]+)(\\d+)$/', $range, $matches) !== 1) { + throw new Exception('Merge must be on a valid range of cells.'); + } - // get the cells in the range - $aReferences = Coordinate::extractAllCellReferencesInRange($range); + $this->mergeCells[$range] = $range; + $firstRow = (int) $matches[2]; + $lastRow = (int) $matches[4]; + $firstColumn = $matches[1]; + $lastColumn = $matches[3]; + $firstColumnIndex = Coordinate::columnIndexFromString($firstColumn); + $lastColumnIndex = Coordinate::columnIndexFromString($lastColumn); + $numberRows = $lastRow - $firstRow; + $numberColumns = $lastColumnIndex - $firstColumnIndex; - // create upper left cell if it does not already exist - $upperLeft = $aReferences[0]; - if (!$this->cellExists($upperLeft)) { - $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); - } + if ($numberRows === 1 && $numberColumns === 1) { + return $this; + } + // create upper left cell if it does not already exist + $upperLeft = "{$firstColumn}{$firstRow}"; + if (!$this->cellExists($upperLeft)) { + $this->getCell($upperLeft)->setValueExplicit(null, DataType::TYPE_NULL); + } + + if ($behaviour !== self::MERGE_CELL_CONTENT_HIDE) { // Blank out the rest of the cells in the range (if they exist) - $count = count($aReferences); - for ($i = 1; $i < $count; ++$i) { - if ($this->cellExists($aReferences[$i])) { - $this->getCell($aReferences[$i])->setValueExplicit(null, DataType::TYPE_NULL); - } + if ($numberRows > $numberColumns) { + $this->clearMergeCellsByColumn($firstColumn, $lastColumn, $firstRow, $lastRow, $upperLeft, $behaviour); + } else { + $this->clearMergeCellsByRow($firstColumn, $lastColumnIndex, $firstRow, $lastRow, $upperLeft, $behaviour); } - } else { - throw new Exception('Merge must be set on a range of cells.'); } return $this; } + private function clearMergeCellsByColumn(string $firstColumn, string $lastColumn, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void + { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + + foreach ($this->getColumnIterator($firstColumn, $lastColumn) as $column) { + $iterator = $column->getCellIterator($firstRow); + $iterator->setIterateOnlyExistingCells(true); + foreach ($iterator as $cell) { + if ($cell !== null) { + $row = $cell->getRow(); + if ($row > $lastRow) { + break; + } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); + } + } + } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } + } + + private function clearMergeCellsByRow(string $firstColumn, int $lastColumnIndex, int $firstRow, int $lastRow, string $upperLeft, string $behaviour): void + { + $leftCellValue = [$this->getCell($upperLeft)->getFormattedValue()]; + + foreach ($this->getRowIterator($firstRow, $lastRow) as $row) { + $iterator = $row->getCellIterator($firstColumn); + $iterator->setIterateOnlyExistingCells(true); + foreach ($iterator as $cell) { + if ($cell !== null) { + $column = $cell->getColumn(); + $columnIndex = Coordinate::columnIndexFromString($column); + if ($columnIndex > $lastColumnIndex) { + break; + } + $leftCellValue = $this->mergeCellBehaviour($cell, $upperLeft, $behaviour, $leftCellValue); + } + } + } + + $leftCellValue = implode(' ', $leftCellValue); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $this->getCell($upperLeft)->setValueExplicit($leftCellValue, DataType::TYPE_STRING); + } + } + + public function mergeCellBehaviour(Cell $cell, string $upperLeft, string $behaviour, array $leftCellValue): array + { + if ($cell->getCoordinate() !== $upperLeft) { + Calculation::getInstance($cell->getWorksheet()->getParent())->flushInstance(); + if ($behaviour === self::MERGE_CELL_CONTENT_MERGE) { + $cellValue = $cell->getFormattedValue(); + if ($cellValue !== '') { + $leftCellValue[] = $cellValue; + } + } + $cell->setValueExplicit(null, DataType::TYPE_NULL); + } + + return $leftCellValue; + } + /** * Set merge on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the mergeCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell * @param int $row2 Numeric row coordinate of the last cell + * @param string $behaviour How the merged cells should behave. + * Possible values are: + * MERGE_CELL_CONTENT_EMPTY - Empty the content of the hidden cells + * MERGE_CELL_CONTENT_HIDE - Keep the content of the hidden cells + * MERGE_CELL_CONTENT_MERGE - Move the content of the hidden cells into the first cell * * @return $this */ - public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) + public function mergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $behaviour = self::MERGE_CELL_CONTENT_EMPTY) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); - return $this->mergeCells($cellRange); + return $this->mergeCells($cellRange, $behaviour); } /** * Remove merge on a cell range. * - * @param string $range Cell range (e.g. A1:E1) + * @param AddressRange|array|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange. * * @return $this */ public function unmergeCells($range) { - // Uppercase coordinate - $range = strtoupper($range); + $range = Functions::trimSheetFromCellReference(Validations::validateCellRange($range)); if (strpos($range, ':') !== false) { if (isset($this->mergeCells[$range])) { @@ -1763,6 +1938,11 @@ class Worksheet implements IComparable /** * Remove merge on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the unmergeCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell @@ -1772,7 +1952,10 @@ class Worksheet implements IComparable */ public function unmergeCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->unmergeCells($cellRange); } @@ -1803,9 +1986,11 @@ class Worksheet implements IComparable } /** - * Set protection on a cell range. + * Set protection on a cell or cell range. * - * @param string $range Cell (e.g. A1) or cell range (e.g. A1:E1) + * @param AddressRange|array|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. * @param string $password Password to unlock the protection * @param bool $alreadyHashed If the password has already been hashed, set this to true * @@ -1813,8 +1998,7 @@ class Worksheet implements IComparable */ public function protectCells($range, $password, $alreadyHashed = false) { - // Uppercase coordinate - $range = strtoupper($range); + $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range)); if (!$alreadyHashed) { $password = Shared\PasswordHasher::hashPassword($password); @@ -1827,6 +2011,11 @@ class Worksheet implements IComparable /** * Set protection on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the protectCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell @@ -1838,22 +2027,26 @@ class Worksheet implements IComparable */ public function protectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2, $password, $alreadyHashed = false) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->protectCells($cellRange, $password, $alreadyHashed); } /** - * Remove protection on a cell range. + * Remove protection on a cell or cell range. * - * @param string $range Cell (e.g. A1) or cell range (e.g. A1:E1) + * @param AddressRange|array|CellAddress|int|string $range A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. * * @return $this */ public function unprotectCells($range) { - // Uppercase coordinate - $range = strtoupper($range); + $range = Functions::trimSheetFromCellReference(Validations::validateCellOrCellRange($range)); if (isset($this->protectedCells[$range])) { unset($this->protectedCells[$range]); @@ -1867,6 +2060,11 @@ class Worksheet implements IComparable /** * Remove protection on a cell range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the protectCells() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the last cell @@ -1876,7 +2074,10 @@ class Worksheet implements IComparable */ public function unprotectCellsByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) { - $cellRange = Coordinate::stringFromColumnIndex($columnIndex1) . $row1 . ':' . Coordinate::stringFromColumnIndex($columnIndex2) . $row2; + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) + ); return $this->unprotectCells($cellRange); } @@ -1904,17 +2105,21 @@ class Worksheet implements IComparable /** * Set AutoFilter. * - * @param AutoFilter|string $autoFilterOrRange + * @param AddressRange|array|AutoFilter|string $autoFilterOrRange * A simple string containing a Cell range like 'A1:E10' is permitted for backward compatibility + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange. * * @return $this */ public function setAutoFilter($autoFilterOrRange) { - if (is_string($autoFilterOrRange)) { - $this->autoFilter->setRange($autoFilterOrRange); - } elseif (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) { + if (is_object($autoFilterOrRange) && ($autoFilterOrRange instanceof AutoFilter)) { $this->autoFilter = $autoFilterOrRange; + } else { + $cellRange = Functions::trimSheetFromCellReference(Validations::validateCellRange($autoFilterOrRange)); + + $this->autoFilter->setRange($cellRange); } return $this; @@ -1923,6 +2128,11 @@ class Worksheet implements IComparable /** * Set Autofilter Range by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setAutoFilter() method with a cell address range such as 'C5:F8' instead;, + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or an AddressRange object or AutoFilter object. + * * @param int $columnIndex1 Numeric column coordinate of the first cell * @param int $row1 Numeric row coordinate of the first cell * @param int $columnIndex2 Numeric column coordinate of the second cell @@ -1932,11 +2142,12 @@ class Worksheet implements IComparable */ public function setAutoFilterByColumnAndRow($columnIndex1, $row1, $columnIndex2, $row2) { - return $this->setAutoFilter( - Coordinate::stringFromColumnIndex($columnIndex1) . $row1 - . ':' . - Coordinate::stringFromColumnIndex($columnIndex2) . $row2 + $cellRange = new CellRange( + CellAddress::fromColumnAndRow($columnIndex1, $row1), + CellAddress::fromColumnAndRow($columnIndex2, $row2) ); + + return $this->setAutoFilter($cellRange); } /** @@ -1949,6 +2160,58 @@ class Worksheet implements IComparable return $this; } + /** + * Get collection of Tables. + * + * @return ArrayObject + */ + public function getTableCollection() + { + return $this->tableCollection; + } + + /** + * Add Table. + * + * @return $this + */ + public function addTable(Table $table): self + { + $table->setWorksheet($this); + $this->tableCollection[] = $table; + + return $this; + } + + /** + * Remove Table by name. + * + * @param string $name Table name + * + * @return $this + */ + public function removeTableByName(string $name): self + { + $name = Shared\StringHelper::strToUpper($name); + foreach ($this->tableCollection as $key => $table) { + if (Shared\StringHelper::strToUpper($table->getName()) === $name) { + unset($this->tableCollection[$key]); + } + } + + return $this; + } + + /** + * Remove collection of Tables. + */ + public function removeTableCollection(): self + { + $this->tableCollection = new ArrayObject(); + + return $this; + } + /** * Get Freeze Pane. * @@ -1968,23 +2231,33 @@ class Worksheet implements IComparable * - B1 will freeze the columns to the left of cell B1 (i.e column A) * - B2 will freeze the rows above and to the left of cell B2 (i.e row 1 and column A) * - * @param null|string $cell Position of the split - * @param null|string $topLeftCell default position of the right bottom pane + * @param null|array|CellAddress|string $coordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * Passing a null value for this argument will clear any existing freeze pane for this worksheet. + * @param null|array|CellAddress|string $topLeftCell default position of the right bottom pane + * Coordinate of the cell as a string, eg: 'C5'; or as an array of [$columnIndex, $row] (e.g. [3, 5]), + * or a CellAddress object. * * @return $this */ - public function freezePane($cell, $topLeftCell = null) + public function freezePane($coordinate, $topLeftCell = null) { - if (is_string($cell) && Coordinate::coordinateIsRange($cell)) { + $cellAddress = ($coordinate !== null) + ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($coordinate)) + : null; + if ($cellAddress !== null && Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Freeze pane can not be set on a range of cells.'); } + $topLeftCell = ($topLeftCell !== null) + ? Functions::trimSheetFromCellReference(Validations::validateCellAddress($topLeftCell)) + : null; - if ($cell !== null && $topLeftCell === null) { - $coordinate = Coordinate::coordinateFromString($cell); + if ($cellAddress !== null && $topLeftCell === null) { + $coordinate = Coordinate::coordinateFromString($cellAddress); $topLeftCell = $coordinate[0] . $coordinate[1]; } - $this->freezePane = $cell; + $this->freezePane = $cellAddress; $this->topLeftCell = $topLeftCell; return $this; @@ -2000,6 +2273,10 @@ class Worksheet implements IComparable /** * Freeze Pane by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the freezePane() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @@ -2101,6 +2378,7 @@ class Worksheet implements IComparable throw new Exception('Rows to be deleted should at least start from row 1.'); } + $holdRowDimensions = $this->removeRowDimensions($row, $numberOfRows); $highestRow = $this->getHighestDataRow(); $removedRowsCounter = 0; @@ -2118,9 +2396,30 @@ class Worksheet implements IComparable --$highestRow; } + $this->rowDimensions = $holdRowDimensions; + return $this; } + private function removeRowDimensions(int $row, int $numberOfRows): array + { + $highRow = $row + $numberOfRows - 1; + $holdRowDimensions = []; + foreach ($this->rowDimensions as $rowDimension) { + $num = $rowDimension->getRowIndex(); + if ($num < $row) { + $holdRowDimensions[$num] = $rowDimension; + } elseif ($num > $highRow) { + $num -= $numberOfRows; + $cloneDimension = clone $rowDimension; + $cloneDimension->setRowIndex($num); + $holdRowDimensions[$num] = $cloneDimension; + } + } + + return $holdRowDimensions; + } + /** * Remove a column, updating all possible related data. * @@ -2139,14 +2438,18 @@ class Worksheet implements IComparable $highestColumnIndex = Coordinate::columnIndexFromString($highestColumn); $pColumnIndex = Coordinate::columnIndexFromString($column); - if ($pColumnIndex > $highestColumnIndex) { - return $this; - } + $holdColumnDimensions = $this->removeColumnDimensions($pColumnIndex, $numberOfColumns); $column = Coordinate::stringFromColumnIndex($pColumnIndex + $numberOfColumns); $objReferenceHelper = ReferenceHelper::getInstance(); $objReferenceHelper->insertNewBefore($column . '1', -$numberOfColumns, 0, $this); + $this->columnDimensions = $holdColumnDimensions; + + if ($pColumnIndex > $highestColumnIndex) { + return $this; + } + $maxPossibleColumnsToBeRemoved = $highestColumnIndex - $pColumnIndex + 1; for ($c = 0, $n = min($maxPossibleColumnsToBeRemoved, $numberOfColumns); $c < $n; ++$c) { @@ -2159,6 +2462,26 @@ class Worksheet implements IComparable return $this; } + private function removeColumnDimensions(int $pColumnIndex, int $numberOfColumns): array + { + $highCol = $pColumnIndex + $numberOfColumns - 1; + $holdColumnDimensions = []; + foreach ($this->columnDimensions as $columnDimension) { + $num = $columnDimension->getColumnNumeric(); + if ($num < $pColumnIndex) { + $str = $columnDimension->getColumnIndex(); + $holdColumnDimensions[$str] = $columnDimension; + } elseif ($num > $highCol) { + $cloneDimension = clone $columnDimension; + $cloneDimension->setColumnNumeric($num - $numberOfColumns); + $str = $cloneDimension->getColumnIndex(); + $holdColumnDimensions[$str] = $cloneDimension; + } + } + + return $holdColumnDimensions; + } + /** * Remove a column, updating all possible related data. * @@ -2178,10 +2501,8 @@ class Worksheet implements IComparable /** * Show gridlines? - * - * @return bool */ - public function getShowGridlines() + public function getShowGridlines(): bool { return $this->showGridlines; } @@ -2193,7 +2514,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowGridlines($showGridLines) + public function setShowGridlines(bool $showGridLines): self { $this->showGridlines = $showGridLines; @@ -2202,10 +2523,8 @@ class Worksheet implements IComparable /** * Print gridlines? - * - * @return bool */ - public function getPrintGridlines() + public function getPrintGridlines(): bool { return $this->printGridlines; } @@ -2217,7 +2536,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setPrintGridlines($printGridLines) + public function setPrintGridlines(bool $printGridLines): self { $this->printGridlines = $printGridLines; @@ -2226,10 +2545,8 @@ class Worksheet implements IComparable /** * Show row and column headers? - * - * @return bool */ - public function getShowRowColHeaders() + public function getShowRowColHeaders(): bool { return $this->showRowColHeaders; } @@ -2241,7 +2558,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowRowColHeaders($showRowColHeaders) + public function setShowRowColHeaders(bool $showRowColHeaders): self { $this->showRowColHeaders = $showRowColHeaders; @@ -2250,10 +2567,8 @@ class Worksheet implements IComparable /** * Show summary below? (Row/Column outlining). - * - * @return bool */ - public function getShowSummaryBelow() + public function getShowSummaryBelow(): bool { return $this->showSummaryBelow; } @@ -2265,7 +2580,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowSummaryBelow($showSummaryBelow) + public function setShowSummaryBelow(bool $showSummaryBelow): self { $this->showSummaryBelow = $showSummaryBelow; @@ -2274,10 +2589,8 @@ class Worksheet implements IComparable /** * Show summary right? (Row/Column outlining). - * - * @return bool */ - public function getShowSummaryRight() + public function getShowSummaryRight(): bool { return $this->showSummaryRight; } @@ -2289,7 +2602,7 @@ class Worksheet implements IComparable * * @return $this */ - public function setShowSummaryRight($showSummaryRight) + public function setShowSummaryRight(bool $showSummaryRight): self { $this->showSummaryRight = $showSummaryRight; @@ -2313,41 +2626,66 @@ class Worksheet implements IComparable * * @return $this */ - public function setComments(array $comments) + public function setComments(array $comments): self { $this->comments = $comments; return $this; } + /** + * Remove comment from cell. + * + * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * + * @return $this + */ + public function removeComment($cellCoordinate): self + { + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); + + if (Coordinate::coordinateIsRange($cellAddress)) { + throw new Exception('Cell coordinate string can not be a range of cells.'); + } elseif (strpos($cellAddress, '$') !== false) { + throw new Exception('Cell coordinate string must not be absolute.'); + } elseif ($cellAddress == '') { + throw new Exception('Cell coordinate can not be zero-length string.'); + } + // Check if we have a comment for this cell and delete it + if (isset($this->comments[$cellAddress])) { + unset($this->comments[$cellAddress]); + } + + return $this; + } + /** * Get comment for cell. * - * @param string $cellCoordinate Cell coordinate to get comment for, eg: 'A1' - * - * @return Comment + * @param array|CellAddress|string $cellCoordinate Coordinate of the cell as a string, eg: 'C5'; + * or as an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. */ - public function getComment($cellCoordinate) + public function getComment($cellCoordinate): Comment { - // Uppercase coordinate - $cellCoordinate = strtoupper($cellCoordinate); + $cellAddress = Functions::trimSheetFromCellReference(Validations::validateCellAddress($cellCoordinate)); - if (Coordinate::coordinateIsRange($cellCoordinate)) { + if (Coordinate::coordinateIsRange($cellAddress)) { throw new Exception('Cell coordinate string can not be a range of cells.'); - } elseif (strpos($cellCoordinate, '$') !== false) { + } elseif (strpos($cellAddress, '$') !== false) { throw new Exception('Cell coordinate string must not be absolute.'); - } elseif ($cellCoordinate == '') { + } elseif ($cellAddress == '') { throw new Exception('Cell coordinate can not be zero-length string.'); } // Check if we already have a comment for this cell. - if (isset($this->comments[$cellCoordinate])) { - return $this->comments[$cellCoordinate]; + if (isset($this->comments[$cellAddress])) { + return $this->comments[$cellAddress]; } // If not, create a new comment. $newComment = new Comment(); - $this->comments[$cellCoordinate] = $newComment; + $this->comments[$cellAddress] = $newComment; return $newComment; } @@ -2355,12 +2693,14 @@ class Worksheet implements IComparable /** * Get comment for cell by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the getComment() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell - * - * @return Comment */ - public function getCommentByColumnAndRow($columnIndex, $row) + public function getCommentByColumnAndRow($columnIndex, $row): Comment { return $this->getComment(Coordinate::stringFromColumnIndex($columnIndex) . $row); } @@ -2397,64 +2737,21 @@ class Worksheet implements IComparable return $this->setSelectedCells($coordinate); } - /** - * Sigh - Phpstan thinks, correctly, that preg_replace can return null. - * But Scrutinizer doesn't. Try to satisfy both. - * - * @param mixed $str - */ - private static function ensureString($str): string - { - return is_string($str) ? $str : ''; - } - - public static function pregReplace(string $pattern, string $replacement, string $subject): string - { - return self::ensureString(preg_replace($pattern, $replacement, $subject)); - } - - private function tryDefinedName(string $coordinate): string - { - // Uppercase coordinate - $coordinate = strtoupper($coordinate); - // Eliminate leading equal sign - $coordinate = self::pregReplace('/^=/', '', $coordinate); - $defined = $this->parent->getDefinedName($coordinate, $this); - if ($defined !== null) { - if ($defined->getWorksheet() === $this && !$defined->isFormula()) { - $coordinate = self::pregReplace('/^=/', '', $defined->getValue()); - } - } - - return $coordinate; - } - /** * Select a range of cells. * - * @param string $coordinate Cell range, examples: 'A1', 'B2:G5', 'A:C', '3:6' + * @param AddressRange|array|CellAddress|int|string $coordinate A simple string containing a Cell range like 'A1:E10' + * or passing in an array of [$fromColumnIndex, $fromRow, $toColumnIndex, $toRow] (e.g. [3, 5, 6, 8]), + * or a CellAddress or AddressRange object. * * @return $this */ public function setSelectedCells($coordinate) { - $originalCoordinate = $coordinate; - $coordinate = $this->tryDefinedName($coordinate); - - // Convert 'A' to 'A:A' - $coordinate = self::pregReplace('/^([A-Z]+)$/', '${1}:${1}', $coordinate); - - // Convert '1' to '1:1' - $coordinate = self::pregReplace('/^(\d+)$/', '${1}:${1}', $coordinate); - - // Convert 'A:C' to 'A1:C1048576' - $coordinate = self::pregReplace('/^([A-Z]+):([A-Z]+)$/', '${1}1:${2}1048576', $coordinate); - - // Convert '1:3' to 'A1:XFD3' - $coordinate = self::pregReplace('/^(\d+):(\d+)$/', 'A${1}:XFD${2}', $coordinate); - if (preg_match('/^\\$?[A-Z]{1,3}\\$?\d{1,7}(:\\$?[A-Z]{1,3}\\$?\d{1,7})?$/', $coordinate) !== 1) { - throw new Exception("Invalid setSelectedCells $originalCoordinate $coordinate"); + if (is_string($coordinate)) { + $coordinate = Validations::definedNameToCoordinate($coordinate, $this); } + $coordinate = Validations::validateCellOrCellRange($coordinate); if (Coordinate::coordinateIsRange($coordinate)) { [$first] = Coordinate::splitRange($coordinate); @@ -2470,6 +2767,10 @@ class Worksheet implements IComparable /** * Selected cell by using numeric cell coordinates. * + * @Deprecated 1.23.0 + * Use the setSelectedCells() method with a cell address such as 'C5' instead;, + * or passing in an array of [$columnIndex, $row] (e.g. [3, 5]), or a CellAddress object. + * * @param int $columnIndex Numeric column coordinate of the cell * @param int $row Numeric row coordinate of the cell * @@ -2665,6 +2966,7 @@ class Worksheet implements IComparable { $namedRange = $this->validateNamedRange($definedName); $workSheet = $namedRange->getWorksheet(); + /** @phpstan-ignore-next-line */ $cellRange = ltrim(substr($namedRange->getValue(), strrpos($namedRange->getValue(), '!')), '!'); $cellRange = str_replace('$', '', $cellRange); @@ -2777,7 +3079,11 @@ class Worksheet implements IComparable * Extract worksheet title from range. * * Example: extractSheetTitle("testSheet!A1") ==> 'A1' + * Example: extractSheetTitle("testSheet!A1:C3") ==> 'A1:C3' * Example: extractSheetTitle("'testSheet 1'!A1", true) ==> ['testSheet 1', 'A1']; + * Example: extractSheetTitle("'testSheet 1'!A1:C3", true) ==> ['testSheet 1', 'A1:C3']; + * Example: extractSheetTitle("A1", true) ==> ['', 'A1']; + * Example: extractSheetTitle("A1:C3", true) ==> ['', 'A1:C3'] * * @param string $range Range to extract title from * @param bool $returnRange Return range? (see example) @@ -2786,6 +3092,10 @@ class Worksheet implements IComparable */ public static function extractSheetTitle($range, $returnRange = false) { + if ($range === null) { + return $returnRange ? [null, null] : null; + } + // Sheet title included? if (($sep = strrpos($range, '!')) === false) { return $returnRange ? ['', $range] : ''; @@ -3000,6 +3310,66 @@ class Worksheet implements IComparable return clone $this; } + /** + * Returns a boolean true if the specified row contains no cells. By default, this means that no cell records + * exist in the collection for this row. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the row will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the row will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the row + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyRow(int $rowId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new RowIterator($this, $rowId, $rowId); + $iterator->seek($rowId); + $row = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $row->isEmpty($definitionOfEmptyFlags); + } + + /** + * Returns a boolean true if the specified column contains no cells. By default, this means that no cell records + * exist in the collection for this column. false will be returned otherwise. + * This rule can be modified by passing a $definitionOfEmptyFlags value: + * 1 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL If the only cells in the collection are null value + * cells, then the column will be considered empty. + * 2 - CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL If the only cells in the collection are empty + * string value cells, then the column will be considered empty. + * 3 - CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL | CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + * If the only cells in the collection are null value or empty string value cells, then the column + * will be considered empty. + * + * @param int $definitionOfEmptyFlags + * Possible Flag Values are: + * CellIterator::TREAT_NULL_VALUE_AS_EMPTY_CELL + * CellIterator::TREAT_EMPTY_STRING_AS_EMPTY_CELL + */ + public function isEmptyColumn(string $columnId, int $definitionOfEmptyFlags = 0): bool + { + try { + $iterator = new ColumnIterator($this, $columnId, $columnId); + $iterator->seek($columnId); + $column = $iterator->current(); + } catch (Exception $e) { + return true; + } + + return $column->isEmpty($definitionOfEmptyFlags); + } + /** * Implement PHP __clone to create a deep clone, not just a shallow copy. */ @@ -3112,4 +3482,9 @@ class Worksheet implements IComparable { return $this->codeName !== null; } + + public static function nameRequiresQuotes(string $sheetName): bool + { + return preg_match(self::SHEET_NAME_REQUIRES_NO_QUOTES, $sheetName) !== 1; + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php index 7811a2a0edc..2ea0ea8b5f4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -115,10 +115,12 @@ abstract class BaseWriter implements IWriter return; } - $mode = 'wb+'; + $mode = 'wb'; $scheme = parse_url($filename, PHP_URL_SCHEME); if ($scheme === 's3') { + // @codeCoverageIgnoreStart $mode = 'w'; + // @codeCoverageIgnoreEnd } $fileHandle = $filename ? fopen($filename, $mode) : false; if ($fileHandle === false) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php index 9169d43bb50..0fef0f6071c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Html.php @@ -24,6 +24,7 @@ use PhpOffice\PhpSpreadsheet\Style\NumberFormat; use PhpOffice\PhpSpreadsheet\Style\Style; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; +use PhpOffice\PhpSpreadsheet\Worksheet\PageSetup; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Html extends BaseWriter @@ -54,7 +55,7 @@ class Html extends BaseWriter * * @var bool */ - private $embedImages = false; + protected $embedImages = false; /** * Use inline CSS? @@ -126,6 +127,13 @@ class Html extends BaseWriter */ protected $isPdf = false; + /** + * Is the current writer creating mPDF? + * + * @var bool + */ + protected $isMPdf = false; + /** * Generate the Navigation block. * @@ -223,13 +231,6 @@ class Html extends BaseWriter $this->editHtmlCallback = $callback; } - const VALIGN_ARR = [ - Alignment::VERTICAL_BOTTOM => 'bottom', - Alignment::VERTICAL_TOP => 'top', - Alignment::VERTICAL_CENTER => 'middle', - Alignment::VERTICAL_JUSTIFY => 'middle', - ]; - /** * Map VAlign. * @@ -239,17 +240,9 @@ class Html extends BaseWriter */ private function mapVAlign($vAlign) { - return array_key_exists($vAlign, self::VALIGN_ARR) ? self::VALIGN_ARR[$vAlign] : 'baseline'; + return Alignment::VERTICAL_ALIGNMENT_FOR_HTML[$vAlign] ?? ''; } - const HALIGN_ARR = [ - Alignment::HORIZONTAL_LEFT => 'left', - Alignment::HORIZONTAL_RIGHT => 'right', - Alignment::HORIZONTAL_CENTER => 'center', - Alignment::HORIZONTAL_CENTER_CONTINUOUS => 'center', - Alignment::HORIZONTAL_JUSTIFY => 'justify', - ]; - /** * Map HAlign. * @@ -259,7 +252,7 @@ class Html extends BaseWriter */ private function mapHAlign($hAlign) { - return array_key_exists($hAlign, self::HALIGN_ARR) ? self::HALIGN_ARR[$hAlign] : ''; + return Alignment::HORIZONTAL_ALIGNMENT_FOR_HTML[$hAlign] ?? ''; } const BORDER_ARR = [ @@ -280,7 +273,7 @@ class Html extends BaseWriter /** * Map border style. * - * @param int $borderStyle Sheet index + * @param int|string $borderStyle Sheet index * * @return string */ @@ -347,7 +340,7 @@ class Html extends BaseWriter return $this; } - private static function generateMeta($val, $desc) + private static function generateMeta(?string $val, string $desc): string { return $val ? (' ' . PHP_EOL) @@ -391,7 +384,7 @@ class Html extends BaseWriter return $html; } - private function generateSheetPrep() + private function generateSheetPrep(): array { // Ensure that Spans have been calculated? $this->calculateSpans(); @@ -406,7 +399,7 @@ class Html extends BaseWriter return $sheets; } - private function generateSheetStarts($sheet, $rowMin) + private function generateSheetStarts(Worksheet $sheet, int $rowMin): array { // calculate start of , $tbodyStart = $rowMin; @@ -425,7 +418,7 @@ class Html extends BaseWriter return [$theadStart, $theadEnd, $tbodyStart]; } - private function generateSheetTags($row, $theadStart, $theadEnd, $tbodyStart) + private function generateSheetTags(int $row, int $theadStart, int $theadEnd, int $tbodyStart): array { // ? $startTag = ($row == $theadStart) ? (' ' . PHP_EOL) : ''; @@ -477,18 +470,15 @@ class Html extends BaseWriter $column = $minCol; while ($column <= $maxCol) { // Cell exists? - if ($sheet->cellExistsByColumnAndRow($column, $row)) { - $rowData[$column] = Coordinate::stringFromColumnIndex($column) . $row; - } else { - $rowData[$column] = ''; - } - ++$column; + $cellAddress = Coordinate::stringFromColumnIndex($column) . $row; + $rowData[$column++] = ($sheet->getCellCollection()->has($cellAddress)) ? $cellAddress : ''; } $html .= $this->generateRow($sheet, $rowData, $row - 1, $cellType); } $html .= $endTag; } + --$row; $html .= $this->extendRowsForChartsAndImages($sheet, $row); // Write table footer @@ -547,15 +537,10 @@ class Html extends BaseWriter * Extend Row if chart is placed after nominal end of row. * This code should be exercised by sample: * Chart/32_Chart_read_write_PDF.php. - * However, that test is suppressed due to out-of-date - * Jpgraph code issuing warnings. So, don't measure - * code coverage for this function till that is fixed. * * @param int $row Row to check for charts * * @return array - * - * @codeCoverageIgnore */ private function extendRowsForCharts(Worksheet $worksheet, int $row) { @@ -631,11 +616,12 @@ class Html extends BaseWriter * * @return string */ - public static function winFileToUrl($filename) + public static function winFileToUrl($filename, bool $mpdf = false) { // Windows filename if (substr($filename, 1, 2) === ':\\') { - $filename = 'file:///' . str_replace('\\', '/', $filename); + $protocol = $mpdf ? '' : 'file:///'; + $filename = $protocol . str_replace('\\', '/', $filename); } return $filename; @@ -665,24 +651,24 @@ class Html extends BaseWriter $filename = $drawing->getPath(); // Strip off eventual '.' - $filename = preg_replace('/^[.]/', '', $filename); + $filename = (string) preg_replace('/^[.]/', '', $filename); // Prepend images root $filename = $this->getImagesRoot() . $filename; // Strip off eventual '.' if followed by non-/ - $filename = preg_replace('@^[.]([^/])@', '$1', $filename); + $filename = (string) preg_replace('@^[.]([^/])@', '$1', $filename); // Convert UTF8 data to PCDATA $filename = htmlspecialchars($filename, Settings::htmlEntityFlags()); $html .= PHP_EOL; - $imageData = self::winFileToUrl($filename); + $imageData = self::winFileToUrl($filename, $this->isMPdf); - if ($this->embedImages && !$this->isPdf) { + if ($this->embedImages || substr($imageData, 0, 6) === 'zip://') { $picture = @file_get_contents($filename); if ($picture !== false) { - $imageDetails = getimagesize($filename); + $imageDetails = getimagesize($filename) ?: []; // base64 encode the binary data $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; @@ -698,10 +684,10 @@ class Html extends BaseWriter if ($imageResource) { ob_start(); // Let's start output buffering. imagepng($imageResource); // This will normally output the image, but because of ob_start(), it won't. - $contents = ob_get_contents(); // Instead, output above is saved to $contents + $contents = (string) ob_get_contents(); // Instead, output above is saved to $contents ob_end_clean(); // End the output buffer. - $dataUri = 'data:image/jpeg;base64,' . base64_encode($contents); + $dataUri = 'data:image/png;base64,' . base64_encode($contents); // Because of the nature of tables, width is more important than height. // max-width: 100% ensures that image doesnt overflow containing cell @@ -719,11 +705,6 @@ class Html extends BaseWriter * Generate chart tag in cell. * This code should be exercised by sample: * Chart/32_Chart_read_write_PDF.php. - * However, that test is suppressed due to out-of-date - * Jpgraph code issuing warnings. So, don't measure - * code coverage for this function till that is fixed. - * - * @codeCoverageIgnore */ private function writeChartInCell(Worksheet $worksheet, string $coordinates): string { @@ -741,21 +722,18 @@ class Html extends BaseWriter } $html .= PHP_EOL; - $imageDetails = getimagesize($chartFileName); + $imageDetails = getimagesize($chartFileName) ?: []; $filedesc = $chart->getTitle(); $filedesc = $filedesc ? $filedesc->getCaptionText() : ''; $filedesc = $filedesc ? htmlspecialchars($filedesc, ENT_QUOTES) : 'Embedded chart'; - if ($fp = fopen($chartFileName, 'rb', 0)) { - $picture = fread($fp, filesize($chartFileName)); - fclose($fp); - // base64 encode the binary data + $picture = file_get_contents($chartFileName); + if ($picture !== false) { $base64 = base64_encode($picture); $imageData = 'data:' . $imageDetails['mime'] . ';base64,' . $base64; $html .= '' . $filedesc . '' . PHP_EOL; - - unlink($chartFileName); } + unlink($chartFileName); } } } @@ -943,8 +921,8 @@ class Html extends BaseWriter // Calculate cell style hashes foreach ($this->spreadsheet->getCellXfCollection() as $index => $style) { - $css['td.style' . $index] = $this->createCSSStyle($style); - $css['th.style' . $index] = $this->createCSSStyle($style); + $css['td.style' . $index . ', th.style' . $index] = $this->createCSSStyle($style); + //$css['th.style' . $index] = $this->createCSSStyle($style); } // Fetch sheets @@ -996,14 +974,25 @@ class Html extends BaseWriter $css = []; // Create CSS - $css['vertical-align'] = $this->mapVAlign($alignment->getVertical()); - $textAlign = $this->mapHAlign($alignment->getHorizontal()); + $verticalAlign = $this->mapVAlign($alignment->getVertical() ?? ''); + if ($verticalAlign) { + $css['vertical-align'] = $verticalAlign; + } + $textAlign = $this->mapHAlign($alignment->getHorizontal() ?? ''); if ($textAlign) { $css['text-align'] = $textAlign; if (in_array($textAlign, ['left', 'right'])) { $css['padding-' . $textAlign] = (string) ((int) $alignment->getIndent() * 9) . 'px'; } } + $rotation = $alignment->getTextRotation(); + if ($rotation !== 0 && $rotation !== Alignment::TEXTROTATION_STACK_PHPSPREADSHEET) { + if ($this->isMPdf) { + $css['text-rotate'] = "$rotation"; + } else { + $css['transform'] = "rotate({$rotation}deg)"; + } + } return $css; } @@ -1065,10 +1054,8 @@ class Html extends BaseWriter * Create CSS style. * * @param Border $border Border - * - * @return string */ - private function createCSSStyleBorder(Border $border) + private function createCSSStyleBorder(Border $border): string { // Create CSS - add !important to non-none border styles for merged cells $borderStyle = $this->mapBorderStyle($border->getBorderStyle()); @@ -1089,9 +1076,11 @@ class Html extends BaseWriter $css = []; // Create CSS - $value = $fill->getFillType() == Fill::FILL_NONE ? - 'white' : '#' . $fill->getStartColor()->getRGB(); - $css['background-color'] = $value; + if ($fill->getFillType() !== Fill::FILL_NONE) { + $value = $fill->getFillType() == Fill::FILL_NONE ? + 'white' : '#' . $fill->getStartColor()->getRGB(); + $css['background-color'] = $value; + } return $css; } @@ -1099,7 +1088,7 @@ class Html extends BaseWriter /** * Generate HTML footer. */ - public function generateHTMLFooter() + public function generateHTMLFooter(): string { // Construct HTML $html = ''; @@ -1109,7 +1098,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTagInline(Worksheet $worksheet, $id) + private function generateTableTagInline(Worksheet $worksheet, string $id): string { $style = isset($this->cssStyles['table']) ? $this->assembleCSS($this->cssStyles['table']) : ''; @@ -1129,7 +1118,7 @@ class Html extends BaseWriter return $html; } - private function generateTableTag(Worksheet $worksheet, $id, &$html, $sheetIndex): void + private function generateTableTag(Worksheet $worksheet, string $id, string &$html, int $sheetIndex): void { if (!$this->useInlineCss) { $gridlines = $worksheet->getShowGridlines() ? ' gridlines' : ''; @@ -1156,9 +1145,9 @@ class Html extends BaseWriter $html = ''; $id = $showid ? "id='sheet$sheetIndex'" : ''; if ($showid) { - $html .= "

\n"; + $html .= "
" . PHP_EOL; } else { - $html .= "
\n"; + $html .= "
" . PHP_EOL; } $this->generateTableTag($worksheet, $id, $html, $sheetIndex); @@ -1182,7 +1171,7 @@ class Html extends BaseWriter /** * Generate table footer. */ - private function generateTableFooter() + private function generateTableFooter(): string { return ' ' . PHP_EOL . '
' . PHP_EOL; } @@ -1228,9 +1217,9 @@ class Html extends BaseWriter return $html; } - private function generateRowCellCss(Worksheet $worksheet, $cellAddress, $row, $columnNumber) + private function generateRowCellCss(Worksheet $worksheet, string $cellAddress, int $row, int $columnNumber): array { - $cell = ($cellAddress > '') ? $worksheet->getCell($cellAddress) : ''; + $cell = ($cellAddress > '') ? $worksheet->getCellCollection()->get($cellAddress) : ''; $coordinate = Coordinate::stringFromColumnIndex($columnNumber + 1) . ($row + 1); if (!$this->useInlineCss) { $cssClass = 'column' . $columnNumber; @@ -1254,22 +1243,24 @@ class Html extends BaseWriter return [$cell, $cssClass, $coordinate]; } - private function generateRowCellDataValueRich($cell, &$cellData): void + private function generateRowCellDataValueRich(Cell $cell, string &$cellData): void { // Loop through rich text elements $elements = $cell->getValue()->getRichTextElements(); foreach ($elements as $element) { // Rich text start? if ($element instanceof Run) { - $cellData .= ''; - $cellEnd = ''; - if ($element->getFont()->getSuperscript()) { - $cellData .= ''; - $cellEnd = ''; - } elseif ($element->getFont()->getSubscript()) { - $cellData .= ''; - $cellEnd = ''; + if ($element->getFont() !== null) { + $cellData .= ''; + + if ($element->getFont()->getSuperscript()) { + $cellData .= ''; + $cellEnd = ''; + } elseif ($element->getFont()->getSubscript()) { + $cellData .= ''; + $cellEnd = ''; + } } // Convert UTF8 data to PCDATA @@ -1287,23 +1278,22 @@ class Html extends BaseWriter } } - private function generateRowCellDataValue(Worksheet $worksheet, $cell, &$cellData): void + private function generateRowCellDataValue(Worksheet $worksheet, Cell $cell, string &$cellData): void { if ($cell->getValue() instanceof RichText) { $this->generateRowCellDataValueRich($cell, $cellData); } else { $origData = $this->preCalculateFormulas ? $cell->getCalculatedValue() : $cell->getValue(); $formatCode = $worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getNumberFormat()->getFormatCode(); - if ($formatCode !== null) { - $cellData = NumberFormat::toFormattedString( - $origData, - $formatCode, - [$this, 'formatColor'] - ); - } + + $cellData = NumberFormat::toFormattedString( + $origData ?? '', + $formatCode ?? NumberFormat::FORMAT_GENERAL, + [$this, 'formatColor'] + ); if ($cellData === $origData) { - $cellData = htmlspecialchars($cellData ?? '', Settings::htmlEntityFlags()); + $cellData = htmlspecialchars($cellData, Settings::htmlEntityFlags()); } if ($worksheet->getParent()->getCellXfByIndex($cell->getXfIndex())->getFont()->getSuperscript()) { $cellData = '' . $cellData . ''; @@ -1313,7 +1303,11 @@ class Html extends BaseWriter } } - private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, $cellType) + /** + * @param null|Cell|string $cell + * @param array|string $cssClass + */ + private function generateRowCellData(Worksheet $worksheet, $cell, &$cssClass, string $cellType): string { $cellData = ' '; if ($cell instanceof Cell) { @@ -1327,16 +1321,16 @@ class Html extends BaseWriter // Converts the cell content so that spaces occuring at beginning of each new line are replaced by   // Example: " Hello\n to the world" is converted to "  Hello\n to the world" - $cellData = preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); + $cellData = (string) preg_replace('/(?m)(?:^|\\G) /', ' ', $cellData); // convert newline "\n" to '
' $cellData = nl2br($cellData); // Extend CSS class? - if (!$this->useInlineCss) { + if (!$this->useInlineCss && is_string($cssClass)) { $cssClass .= ' style' . $cell->getXfIndex(); $cssClass .= ' ' . $cell->getDataType(); - } else { + } elseif (is_array($cssClass)) { if ($cellType == 'th') { if (isset($this->cssStyles['th.style' . $cell->getXfIndex()])) { $cssClass = array_merge($cssClass, $this->cssStyles['th.style' . $cell->getXfIndex()]); @@ -1366,12 +1360,12 @@ class Html extends BaseWriter return $cellData; } - private function generateRowIncludeCharts(Worksheet $worksheet, $coordinate) + private function generateRowIncludeCharts(Worksheet $worksheet, string $coordinate): string { return $this->includeCharts ? $this->writeChartInCell($worksheet, $coordinate) : ''; } - private function generateRowSpans($html, $rowSpan, $colSpan) + private function generateRowSpans(string $html, int $rowSpan, int $colSpan): string { $html .= ($colSpan > 1) ? (' colspan="' . $colSpan . '"') : ''; $html .= ($rowSpan > 1) ? (' rowspan="' . $rowSpan . '"') : ''; @@ -1379,7 +1373,10 @@ class Html extends BaseWriter return $html; } - private function generateRowWriteCell(&$html, Worksheet $worksheet, $coordinate, $cellType, $cellData, $colSpan, $rowSpan, $cssClass, $colNum, $sheetIndex, $row): void + /** + * @param array|string $cssClass + */ + private function generateRowWriteCell(string &$html, Worksheet $worksheet, string $coordinate, string $cellType, string $cellData, int $colSpan, int $rowSpan, $cssClass, int $colNum, int $sheetIndex, int $row): void { // Image? $htmlx = $this->writeImageInCell($worksheet, $coordinate); @@ -1387,7 +1384,7 @@ class Html extends BaseWriter $htmlx .= $this->generateRowIncludeCharts($worksheet, $coordinate); // Column start $html .= ' <' . $cellType; - if (!$this->useInlineCss && !$this->isPdf) { + if (!$this->useInlineCss && !$this->isPdf && is_string($cssClass)) { $html .= ' class="' . $cssClass . '"'; if ($htmlx) { $html .= " style='position: relative;'"; @@ -1397,9 +1394,11 @@ class Html extends BaseWriter // We must explicitly write the width of the element because TCPDF // does not recognize e.g. if ($this->useInlineCss) { - $xcssClass = $cssClass; + $xcssClass = is_array($cssClass) ? $cssClass : []; } else { - $html .= ' class="' . $cssClass . '"'; + if (is_string($cssClass)) { + $html .= ' class="' . $cssClass . '"'; + } $xcssClass = []; } $width = 0; @@ -1410,8 +1409,7 @@ class Html extends BaseWriter $width += $this->columnWidths[$sheetIndex][$i]; } } - $xcssClass['width'] = $width . 'pt'; - + $xcssClass['width'] = (string) $width . 'pt'; // We must also explicitly write the height of the element because TCPDF // does not recognize e.g. if (isset($this->cssStyles['table.sheet' . $sheetIndex . ' tr.row' . $row]['height'])) { @@ -1453,15 +1451,16 @@ class Html extends BaseWriter // Sheet index $sheetIndex = $worksheet->getParent()->getIndex($worksheet); $html = $this->generateRowStart($worksheet, $sheetIndex, $row); + $generateDiv = $this->isMPdf && $worksheet->getRowDimension($row + 1)->getVisible() === false; + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } // Write cells $colNum = 0; foreach ($values as $cellAddress) { [$cell, $cssClass, $coordinate] = $this->generateRowCellCss($worksheet, $cellAddress, $row, $colNum); - $colSpan = 1; - $rowSpan = 1; - // Cell Data $cellData = $this->generateRowCellData($worksheet, $cell, $cssClass, $cellType); @@ -1475,8 +1474,8 @@ class Html extends BaseWriter && $this->isSpannedCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]); // Colspan and Rowspan - $colspan = 1; - $rowspan = 1; + $colSpan = 1; + $rowSpan = 1; if (isset($this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum])) { $spans = $this->isBaseCell[$worksheet->getParent()->getIndex($worksheet)][$row + 1][$colNum]; $rowSpan = $spans['rowspan']; @@ -1500,6 +1499,9 @@ class Html extends BaseWriter } // Write row end + if ($generateDiv) { + $html .= '
' . PHP_EOL; + } $html .= ' ' . PHP_EOL; // Return @@ -1723,7 +1725,7 @@ class Html extends BaseWriter $this->spansAreCalculated = true; } - private function calculateSpansOmitRows($sheet, $sheetIndex, $candidateSpannedRow): void + private function calculateSpansOmitRows(Worksheet $sheet, int $sheetIndex, array $candidateSpannedRow): void { // Identify which rows should be omitted in HTML. These are the rows where all the cells // participate in a merge and the where base cells are somewhere above. @@ -1745,7 +1747,7 @@ class Html extends BaseWriter while ($c++ < $e) { $baseCell = $this->isSpannedCell[$sheetIndex][$rowIndex][$c]['baseCell']; - if (!in_array($baseCell, $adjustedBaseCells)) { + if (!in_array($baseCell, $adjustedBaseCells, true)) { // subtract rowspan by 1 --$this->isBaseCell[$sheetIndex][$baseCell[0]][$baseCell[1]]['rowspan']; $adjustedBaseCells[] = $baseCell; @@ -1786,7 +1788,8 @@ class Html extends BaseWriter public function getOrientation(): ?string { - return null; + // Expect Pdf classes to override this method. + return $this->isPdf ? PageSetup::ORIENTATION_PORTRAIT : null; } /** @@ -1825,31 +1828,31 @@ class Html extends BaseWriter $bottom = StringHelper::FormatNumber($worksheet->getPageMargins()->getBottom()) . 'in; '; $htmlPage .= 'margin-bottom: ' . $bottom; $orientation = $this->getOrientation() ?? $worksheet->getPageSetup()->getOrientation(); - if ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_LANDSCAPE) { + if ($orientation === PageSetup::ORIENTATION_LANDSCAPE) { $htmlPage .= 'size: landscape; '; - } elseif ($orientation === \PhpOffice\PhpSpreadsheet\Worksheet\PageSetup::ORIENTATION_PORTRAIT) { + } elseif ($orientation === PageSetup::ORIENTATION_PORTRAIT) { $htmlPage .= 'size: portrait; '; } - $htmlPage .= "}\n"; + $htmlPage .= '}' . PHP_EOL; ++$sheetId; } - $htmlPage .= <<div {margin-top: 5px;} - body>div:first-child {margin-top: 0;} - .scrpgbrk {margin-top: 1px;} -} -@media print { - .gridlinesp td {border: 1px solid black;} - .gridlinesp th {border: 1px solid black;} - .navigation {display: none;} -} - -EOF; + $htmlPage .= implode(PHP_EOL, [ + '.navigation {page-break-after: always;}', + '.scrpgbrk, div + div {page-break-before: always;}', + '@media screen {', + ' .gridlines td {border: 1px solid black;}', + ' .gridlines th {border: 1px solid black;}', + ' body>div {margin-top: 5px;}', + ' body>div:first-child {margin-top: 0;}', + ' .scrpgbrk {margin-top: 1px;}', + '}', + '@media print {', + ' .gridlinesp td {border: 1px solid black;}', + ' .gridlinesp th {border: 1px solid black;}', + ' .navigation {display: none;}', + '}', + '', + ]); $htmlPage .= $generateSurroundingHTML ? ('' . PHP_EOL) : ''; return $htmlPage; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php index b0a6272602d..d8bf1f0c17e 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/IWriter.php @@ -62,6 +62,8 @@ interface IWriter * Save PhpSpreadsheet to file. * * @param resource|string $filename Name of the file to save + * + * @throws Exception */ public function save($filename, int $flags = 0): void; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods.php index decd82bcd39..827c43b5604 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods.php @@ -132,10 +132,11 @@ class Ods extends BaseWriter $zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write()); $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPartthumbnails()->write()); + // Settings always need to be written before Content; Styles after Content + $zip->addFile('settings.xml', $this->getWriterPartsettings()->write()); $zip->addFile('content.xml', $this->getWriterPartcontent()->write()); $zip->addFile('meta.xml', $this->getWriterPartmeta()->write()); $zip->addFile('mimetype', $this->getWriterPartmimetype()->write()); - $zip->addFile('settings.xml', $this->getWriterPartsettings()->write()); $zip->addFile('styles.xml', $this->getWriterPartstyles()->write()); // Close file diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php index f8aae20c0b7..1bf2c46342c 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Cell/Style.php @@ -2,15 +2,22 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods\Cell; +use PhpOffice\PhpSpreadsheet\Helper\Dimension; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Fill; use PhpOffice\PhpSpreadsheet\Style\Font; use PhpOffice\PhpSpreadsheet\Style\Style as CellStyle; +use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; +use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Style { public const CELL_STYLE_PREFIX = 'ce'; + public const COLUMN_STYLE_PREFIX = 'co'; + public const ROW_STYLE_PREFIX = 'ro'; + public const TABLE_STYLE_PREFIX = 'ta'; private $writer; @@ -159,6 +166,83 @@ class Style $this->writer->endElement(); // Close style:text-properties } + protected function writeColumnProperties(ColumnDimension $columnDimension): void + { + $this->writer->startElement('style:table-column-properties'); + $this->writer->writeAttribute( + 'style:column-width', + round($columnDimension->getWidth(Dimension::UOM_CENTIMETERS), 3) . 'cm' + ); + $this->writer->writeAttribute('fo:break-before', 'auto'); + + // End + $this->writer->endElement(); // Close style:table-column-properties + } + + public function writeColumnStyles(ColumnDimension $columnDimension, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table-column'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s_%d_%d', self::COLUMN_STYLE_PREFIX, $sheetId, $columnDimension->getColumnNumeric()) + ); + + $this->writeColumnProperties($columnDimension); + + // End + $this->writer->endElement(); // Close style:style + } + + protected function writeRowProperties(RowDimension $rowDimension): void + { + $this->writer->startElement('style:table-row-properties'); + $this->writer->writeAttribute( + 'style:row-height', + round($rowDimension->getRowHeight(Dimension::UOM_CENTIMETERS), 3) . 'cm' + ); + $this->writer->writeAttribute('style:use-optimal-row-height', 'false'); + $this->writer->writeAttribute('fo:break-before', 'auto'); + + // End + $this->writer->endElement(); // Close style:table-row-properties + } + + public function writeRowStyles(RowDimension $rowDimension, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table-row'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s_%d_%d', self::ROW_STYLE_PREFIX, $sheetId, $rowDimension->getRowIndex()) + ); + + $this->writeRowProperties($rowDimension); + + // End + $this->writer->endElement(); // Close style:style + } + + public function writeTableStyle(Worksheet $worksheet, int $sheetId): void + { + $this->writer->startElement('style:style'); + $this->writer->writeAttribute('style:family', 'table'); + $this->writer->writeAttribute( + 'style:name', + sprintf('%s%d', self::TABLE_STYLE_PREFIX, $sheetId) + ); + + $this->writer->startElement('style:table-properties'); + + $this->writer->writeAttribute( + 'table:display', + $worksheet->getSheetState() === Worksheet::SHEETSTATE_VISIBLE ? 'true' : 'false' + ); + + $this->writer->endElement(); // Close style:table-properties + $this->writer->endElement(); // Close style:style + } + public function write(CellStyle $style): void { $this->writer->startElement('style:style'); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php index a589e54923e..00ab0643d9f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\Row; +use PhpOffice\PhpSpreadsheet\Worksheet\RowCellIterator; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; use PhpOffice\PhpSpreadsheet\Writer\Exception; use PhpOffice\PhpSpreadsheet\Writer\Ods; @@ -119,14 +120,22 @@ class Content extends WriterPart { $spreadsheet = $this->getParentWriter()->getSpreadsheet(); /** @var Spreadsheet $spreadsheet */ $sheetCount = $spreadsheet->getSheetCount(); - for ($i = 0; $i < $sheetCount; ++$i) { + for ($sheetIndex = 0; $sheetIndex < $sheetCount; ++$sheetIndex) { $objWriter->startElement('table:table'); - $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($i)->getTitle()); + $objWriter->writeAttribute('table:name', $spreadsheet->getSheet($sheetIndex)->getTitle()); + $objWriter->writeAttribute('table:style-name', Style::TABLE_STYLE_PREFIX . (string) ($sheetIndex + 1)); $objWriter->writeElement('office:forms'); - $objWriter->startElement('table:table-column'); - $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); - $objWriter->endElement(); - $this->writeRows($objWriter, $spreadsheet->getSheet($i)); + foreach ($spreadsheet->getSheet($sheetIndex)->getColumnDimensions() as $columnDimension) { + $objWriter->startElement('table:table-column'); + $objWriter->writeAttribute( + 'table:style-name', + sprintf('%s_%d_%d', Style::COLUMN_STYLE_PREFIX, $sheetIndex, $columnDimension->getColumnNumeric()) + ); + $objWriter->writeAttribute('table:default-cell-style-name', 'ce0'); +// $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); + $objWriter->endElement(); + } + $this->writeRows($objWriter, $spreadsheet->getSheet($sheetIndex), $sheetIndex); $objWriter->endElement(); } } @@ -134,47 +143,49 @@ class Content extends WriterPart /** * Write rows of the specified sheet. */ - private function writeRows(XMLWriter $objWriter, Worksheet $sheet): void + private function writeRows(XMLWriter $objWriter, Worksheet $sheet, int $sheetIndex): void { $numberRowsRepeated = self::NUMBER_ROWS_REPEATED_MAX; $span_row = 0; $rows = $sheet->getRowIterator(); - while ($rows->valid()) { + foreach ($rows as $row) { + $cellIterator = $row->getCellIterator(); --$numberRowsRepeated; - $row = $rows->current(); - if ($row->getCellIterator()->valid()) { + if ($cellIterator->valid()) { + $objWriter->startElement('table:table-row'); if ($span_row) { - $objWriter->startElement('table:table-row'); if ($span_row > 1) { $objWriter->writeAttribute('table:number-rows-repeated', $span_row); } $objWriter->startElement('table:table-cell'); - $objWriter->writeAttribute('table:number-columns-repeated', self::NUMBER_COLS_REPEATED_MAX); - $objWriter->endElement(); + $objWriter->writeAttribute('table:number-columns-repeated', (string) self::NUMBER_COLS_REPEATED_MAX); $objWriter->endElement(); $span_row = 0; + } else { + if ($sheet->getRowDimension($row->getRowIndex())->getRowHeight() > 0) { + $objWriter->writeAttribute( + 'table:style-name', + sprintf('%s_%d_%d', Style::ROW_STYLE_PREFIX, $sheetIndex, $row->getRowIndex()) + ); + } + $this->writeCells($objWriter, $cellIterator); } - $objWriter->startElement('table:table-row'); - $this->writeCells($objWriter, $row); $objWriter->endElement(); } else { ++$span_row; } - $rows->next(); } } /** * Write cells of the specified row. */ - private function writeCells(XMLWriter $objWriter, Row $row): void + private function writeCells(XMLWriter $objWriter, RowCellIterator $cells): void { $numberColsRepeated = self::NUMBER_COLS_REPEATED_MAX; $prevColumn = -1; - $cells = $row->getCellIterator(); - while ($cells->valid()) { + foreach ($cells as $cell) { /** @var \PhpOffice\PhpSpreadsheet\Cell\Cell $cell */ - $cell = $cells->current(); $column = Coordinate::columnIndexFromString($cell->getColumn()) - 1; $this->writeCellSpan($objWriter, $column, $prevColumn); @@ -237,8 +248,8 @@ class Content extends WriterPart Comment::write($objWriter, $cell); $objWriter->endElement(); $prevColumn = $column; - $cells->next(); } + $numberColsRepeated = $numberColsRepeated - $prevColumn - 1; if ($numberColsRepeated > 0) { if ($numberColsRepeated > 1) { @@ -275,6 +286,28 @@ class Content extends WriterPart private function writeXfStyles(XMLWriter $writer, Spreadsheet $spreadsheet): void { $styleWriter = new Style($writer); + + $sheetCount = $spreadsheet->getSheetCount(); + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $spreadsheet->getSheet($i); + $styleWriter->writeTableStyle($worksheet, $i + 1); + + $worksheet->calculateColumnWidths(); + foreach ($worksheet->getColumnDimensions() as $columnDimension) { + if ($columnDimension->getWidth() !== -1.0) { + $styleWriter->writeColumnStyles($columnDimension, $i); + } + } + } + for ($i = 0; $i < $sheetCount; ++$i) { + $worksheet = $spreadsheet->getSheet($i); + foreach ($worksheet->getRowDimensions() as $rowDimension) { + if ($rowDimension->getRowHeight() > 0.0) { + $styleWriter->writeRowStyles($rowDimension, $i); + } + } + } + foreach ($spreadsheet->getCellXfCollection() as $style) { $styleWriter->write($style); } @@ -296,7 +329,7 @@ class Content extends WriterPart $columnSpan = Coordinate::columnIndexFromString($end[0]) - Coordinate::columnIndexFromString($start[0]) + 1; $rowSpan = ((int) $end[1]) - ((int) $start[1]) + 1; - $objWriter->writeAttribute('table:number-columns-spanned', $columnSpan); - $objWriter->writeAttribute('table:number-rows-spanned', $rowSpan); + $objWriter->writeAttribute('table:number-columns-spanned', (string) $columnSpan); + $objWriter->writeAttribute('table:number-rows-spanned', (string) $rowSpan); } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php index 047bd410b2d..06445591cd0 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Ods/Settings.php @@ -2,8 +2,11 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Cell\CellAddress; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class Settings extends WriterPart { @@ -45,28 +48,9 @@ class Settings extends WriterPart $objWriter->text('view1'); $objWriter->endElement(); // ViewId $objWriter->startElement('config:config-item-map-named'); - $objWriter->writeAttribute('config:name', 'Tables'); - foreach ($spreadsheet->getWorksheetIterator() as $ws) { - $objWriter->startElement('config:config-item-map-entry'); - $objWriter->writeAttribute('config:name', $ws->getTitle()); - $selected = $ws->getSelectedCells(); - if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) { - $colSel = Coordinate::columnIndexFromString($matches[1]) - 1; - $rowSel = (int) $matches[2] - 1; - $objWriter->startElement('config:config-item'); - $objWriter->writeAttribute('config:name', 'CursorPositionX'); - $objWriter->writeAttribute('config:type', 'int'); - $objWriter->text($colSel); - $objWriter->endElement(); - $objWriter->startElement('config:config-item'); - $objWriter->writeAttribute('config:name', 'CursorPositionY'); - $objWriter->writeAttribute('config:type', 'int'); - $objWriter->text($rowSel); - $objWriter->endElement(); - } - $objWriter->endElement(); // config:config-item-map-entry - } - $objWriter->endElement(); // config:config-item-map-named + + $this->writeAllWorksheetSettings($objWriter, $spreadsheet); + $wstitle = $spreadsheet->getActiveSheet()->getTitle(); $objWriter->startElement('config:config-item'); $objWriter->writeAttribute('config:name', 'ActiveTable'); @@ -85,4 +69,84 @@ class Settings extends WriterPart return $objWriter->getData(); } + + private function writeAllWorksheetSettings(XMLWriter $objWriter, Spreadsheet $spreadsheet): void + { + $objWriter->writeAttribute('config:name', 'Tables'); + + foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { + $this->writeWorksheetSettings($objWriter, $worksheet); + } + + $objWriter->endElement(); // config:config-item-map-entry Tables + } + + private function writeWorksheetSettings(XMLWriter $objWriter, Worksheet $worksheet): void + { + $objWriter->startElement('config:config-item-map-entry'); + $objWriter->writeAttribute('config:name', $worksheet->getTitle()); + + $this->writeSelectedCells($objWriter, $worksheet); + if ($worksheet->getFreezePane() !== null) { + $this->writeFreezePane($objWriter, $worksheet); + } + + $objWriter->endElement(); // config:config-item-map-entry Worksheet + } + + private function writeSelectedCells(XMLWriter $objWriter, Worksheet $worksheet): void + { + $selected = $worksheet->getSelectedCells(); + if (preg_match('/^([a-z]+)([0-9]+)/i', $selected, $matches) === 1) { + $colSel = Coordinate::columnIndexFromString($matches[1]) - 1; + $rowSel = (int) $matches[2] - 1; + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'CursorPositionX'); + $objWriter->writeAttribute('config:type', 'int'); + $objWriter->text((string) $colSel); + $objWriter->endElement(); + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'CursorPositionY'); + $objWriter->writeAttribute('config:type', 'int'); + $objWriter->text((string) $rowSel); + $objWriter->endElement(); + } + } + + private function writeSplitValue(XMLWriter $objWriter, string $splitMode, string $type, string $value): void + { + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', $splitMode); + $objWriter->writeAttribute('config:type', $type); + $objWriter->text($value); + $objWriter->endElement(); + } + + private function writeFreezePane(XMLWriter $objWriter, Worksheet $worksheet): void + { + $freezePane = CellAddress::fromCellAddress($worksheet->getFreezePane()); + if ($freezePane->cellAddress() === 'A1') { + return; + } + + $columnId = $freezePane->columnId(); + $columnName = $freezePane->columnName(); + $row = $freezePane->rowId(); + + $this->writeSplitValue($objWriter, 'HorizontalSplitMode', 'short', '2'); + $this->writeSplitValue($objWriter, 'HorizontalSplitPosition', 'int', (string) ($columnId - 1)); + $this->writeSplitValue($objWriter, 'PositionLeft', 'short', '0'); + $this->writeSplitValue($objWriter, 'PositionRight', 'short', (string) ($columnId - 1)); + + for ($column = 'A'; $column !== $columnName; ++$column) { + $worksheet->getColumnDimension($column)->setAutoSize(true); + } + + $this->writeSplitValue($objWriter, 'VerticalSplitMode', 'short', '2'); + $this->writeSplitValue($objWriter, 'VerticalSplitPosition', 'int', (string) ($row - 1)); + $this->writeSplitValue($objWriter, 'PositionTop', 'short', '0'); + $this->writeSplitValue($objWriter, 'PositionBottom', 'short', (string) ($row - 1)); + + $this->writeSplitValue($objWriter, 'ActiveSplitRange', 'short', '3'); + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index fc96f904945..690b0c54a34 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -7,6 +7,13 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Dompdf extends Pdf { + /** + * embed images, or link to images. + * + * @var bool + */ + protected $embedImages = true; + /** * Gets the implementation of external PDF library that should be used. * @@ -26,15 +33,15 @@ class Dompdf extends Pdf { $fileHandle = parent::prepareForSave($filename); - // Default PDF paper size - $paperSize = 'LETTER'; // Letter (8.5 in. by 11 in.) - // Check for paper size and page orientation $setup = $this->spreadsheet->getSheet($this->getSheetIndex() ?? 0)->getPageSetup(); $orientation = $this->getOrientation() ?? $setup->getOrientation(); $orientation = ($orientation === PageSetup::ORIENTATION_LANDSCAPE) ? 'L' : 'P'; $printPaperSize = $this->getPaperSize() ?? $setup->getPaperSize(); $paperSize = self::$paperSizes[$printPaperSize] ?? PageSetup::getPaperSizeDefault(); + if (is_array($paperSize) && count($paperSize) === 2) { + $paperSize = [0.0, 0.0, $paperSize[0], $paperSize[1]]; + } $orientation = ($orientation == 'L') ? 'landscape' : 'portrait'; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index 281e1a4f9b5..d0ce9ed4582 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -8,6 +8,9 @@ use PhpOffice\PhpSpreadsheet\Writer\Pdf; class Mpdf extends Pdf { + /** @var bool */ + protected $isMPdf = true; + /** * Gets the implementation of external PDF library that should be used. * diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index d29d4764876..aefc6b56d02 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -77,7 +77,7 @@ class Tcpdf extends Pdf $pdf->SetCreator($this->spreadsheet->getProperties()->getCreator()); // Write to file - fwrite($fileHandle, $pdf->output($filename, 'S')); + fwrite($fileHandle, $pdf->output('', 'S')); parent::restoreStateAfterSave(); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php index 4f506070526..c9446e7077a 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx.php @@ -25,6 +25,7 @@ use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsRibbon; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\RelsVBA; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\StringTable; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Style; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Table; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Theme; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Workbook; use PhpOffice\PhpSpreadsheet\Writer\Xlsx\Worksheet; @@ -167,6 +168,11 @@ class Xlsx extends BaseWriter */ private $writerPartTheme; + /** + * @var Table + */ + private $writerPartTable; + /** * @var Workbook */ @@ -196,6 +202,7 @@ class Xlsx extends BaseWriter $this->writerPartStringTable = new StringTable($this); $this->writerPartStyle = new Style($this); $this->writerPartTheme = new Theme($this); + $this->writerPartTable = new Table($this); $this->writerPartWorkbook = new Workbook($this); $this->writerPartWorksheet = new Worksheet($this); @@ -271,6 +278,11 @@ class Xlsx extends BaseWriter return $this->writerPartTheme; } + public function getWriterPartTable(): Table + { + return $this->writerPartTable; + } + public function getWriterPartWorkbook(): Workbook { return $this->writerPartWorkbook; @@ -337,12 +349,15 @@ class Xlsx extends BaseWriter //a custom UI in this workbook ? add it ("base" xml and additional objects (pictures) and rels) if ($this->spreadSheet->hasRibbon()) { $tmpRibbonTarget = $this->spreadSheet->getRibbonXMLData('target'); + $tmpRibbonTarget = is_string($tmpRibbonTarget) ? $tmpRibbonTarget : ''; $zipContent[$tmpRibbonTarget] = $this->spreadSheet->getRibbonXMLData('data'); if ($this->spreadSheet->hasRibbonBinObjects()) { $tmpRootPath = dirname($tmpRibbonTarget) . '/'; $ribbonBinObjects = $this->spreadSheet->getRibbonBinObjects('data'); //the files to write - foreach ($ribbonBinObjects as $aPath => $aContent) { - $zipContent[$tmpRootPath . $aPath] = $aContent; + if (is_array($ribbonBinObjects)) { + foreach ($ribbonBinObjects as $aPath => $aContent) { + $zipContent[$tmpRootPath . $aPath] = $aContent; + } } //the rels for files $zipContent[$tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels'] = $this->getWriterPartRelsRibbon()->writeRibbonRelationships($this->spreadSheet); @@ -389,10 +404,11 @@ class Xlsx extends BaseWriter } $chartRef1 = 0; + $tableRef1 = 1; // Add worksheet relationships (drawings, ...) for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { // Add relationships - $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts); + $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts, $tableRef1); // Add unparsedLoadedData $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); @@ -478,6 +494,12 @@ class Xlsx extends BaseWriter $zipContent['xl/media/' . $image->getIndexedFilename()] = file_get_contents($image->getPath()); } } + + // Add Table parts + $tables = $this->spreadSheet->getSheet($i)->getTableCollection(); + foreach ($tables as $table) { + $zipContent['xl/tables/table' . $tableRef1 . '.xml'] = $this->getWriterPartTable()->writeTable($table, $tableRef1++); + } } // Add media @@ -501,8 +523,10 @@ class Xlsx extends BaseWriter $zipContent['xl/media/' . $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()] = $imageContents; } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) { ob_start(); + /** @var callable */ + $callable = $this->getDrawingHashTable()->getByIndex($i)->getRenderingFunction(); call_user_func( - $this->getDrawingHashTable()->getByIndex($i)->getRenderingFunction(), + $callable, $this->getDrawingHashTable()->getByIndex($i)->getImageResource() ); $imageContents = ob_get_contents(); @@ -663,6 +687,7 @@ class Xlsx extends BaseWriter return $this; } + /** @var array */ private $pathNames = []; private function addZipFile(string $path, string $content): void diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 23b78a2f2f3..3b64bd8e9f4 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -3,21 +3,20 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Chart\Axis; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\Chart\DataSeries; use PhpOffice\PhpSpreadsheet\Chart\DataSeriesValues; -use PhpOffice\PhpSpreadsheet\Chart\GridLines; use PhpOffice\PhpSpreadsheet\Chart\Layout; use PhpOffice\PhpSpreadsheet\Chart\Legend; use PhpOffice\PhpSpreadsheet\Chart\PlotArea; +use PhpOffice\PhpSpreadsheet\Chart\Properties; use PhpOffice\PhpSpreadsheet\Chart\Title; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; +use PhpOffice\PhpSpreadsheet\Chart\TrendLine; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; class Chart extends WriterPart { - protected $calculateCellValues; - /** * @var int */ @@ -32,8 +31,6 @@ class Chart extends WriterPart */ public function writeChart(\PhpOffice\PhpSpreadsheet\Chart\Chart $chart, $calculateCellValues = true) { - $this->calculateCellValues = $calculateCellValues; - // Create XML writer $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { @@ -42,7 +39,7 @@ class Chart extends WriterPart $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); } // Ensure that data series values are up-to-date before we save - if ($this->calculateCellValues) { + if ($calculateCellValues) { $chart->refresh(); } @@ -56,13 +53,13 @@ class Chart extends WriterPart $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); $objWriter->startElement('c:date1904'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->startElement('c:lang'); $objWriter->writeAttribute('val', 'en-GB'); $objWriter->endElement(); $objWriter->startElement('c:roundedCorners'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $chart->getRoundedCorners() ? '1' : '0'); $objWriter->endElement(); $this->writeAlternateContent($objWriter); @@ -72,15 +69,34 @@ class Chart extends WriterPart $this->writeTitle($objWriter, $chart->getTitle()); $objWriter->startElement('c:autoTitleDeleted'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', (string) (int) $chart->getAutoTitleDeleted()); $objWriter->endElement(); - $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY(), $chart->getMajorGridlines(), $chart->getMinorGridlines()); + $objWriter->startElement('c:view3D'); + $surface2D = false; + $plotArea = $chart->getPlotArea(); + if ($plotArea !== null) { + $seriesArray = $plotArea->getPlotGroup(); + foreach ($seriesArray as $series) { + if ($series->getPlotType() === DataSeries::TYPE_SURFACECHART) { + $surface2D = true; + + break; + } + } + } + $this->writeView3D($objWriter, $chart->getRotX(), 'c:rotX', $surface2D, 90); + $this->writeView3D($objWriter, $chart->getRotY(), 'c:rotY', $surface2D); + $this->writeView3D($objWriter, $chart->getRAngAx(), 'c:rAngAx', $surface2D); + $this->writeView3D($objWriter, $chart->getPerspective(), 'c:perspective', $surface2D); + $objWriter->endElement(); // view3D + + $this->writePlotArea($objWriter, $chart->getPlotArea(), $chart->getXAxisLabel(), $chart->getYAxisLabel(), $chart->getChartAxisX(), $chart->getChartAxisY()); $this->writeLegend($objWriter, $chart->getLegend()); $objWriter->startElement('c:plotVisOnly'); - $objWriter->writeAttribute('val', (int) $chart->getPlotVisibleOnly()); + $objWriter->writeAttribute('val', (string) (int) $chart->getPlotVisibleOnly()); $objWriter->endElement(); $objWriter->startElement('c:dispBlanksAs'); @@ -88,19 +104,37 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:showDLblsOverMax'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:chart + if ($chart->getNoFill()) { + $objWriter->startElement('c:spPr'); + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + $objWriter->endElement(); // c:spPr + } $this->writePrintSettings($objWriter); - $objWriter->endElement(); + $objWriter->endElement(); // c:chartSpace // Return return $objWriter->getData(); } + private function writeView3D(XMLWriter $objWriter, ?int $value, string $tag, bool $surface2D, int $default = 0): void + { + if ($value === null && $surface2D) { + $value = $default; + } + if ($value !== null) { + $objWriter->startElement($tag); + $objWriter->writeAttribute('val', "$value"); + $objWriter->endElement(); + } + } + /** * Write Chart Title. */ @@ -121,6 +155,10 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); + $objWriter->endElement(); $caption = $title->getCaption(); if ((is_array($caption)) && (count($caption) > 0)) { @@ -135,7 +173,7 @@ class Chart extends WriterPart $this->writeLayout($objWriter, $title->getLayout()); $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -171,7 +209,7 @@ class Chart extends WriterPart $objWriter->startElement('a:p'); $objWriter->startElement('a:pPr'); - $objWriter->writeAttribute('rtl', 0); + $objWriter->writeAttribute('rtl', '0'); $objWriter->startElement('a:defRPr'); $objWriter->endElement(); @@ -190,13 +228,13 @@ class Chart extends WriterPart /** * Write Chart Plot Area. */ - private function writePlotArea(XMLWriter $objWriter, PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null, ?GridLines $majorGridlines = null, ?GridLines $minorGridlines = null): void + private function writePlotArea(XMLWriter $objWriter, ?PlotArea $plotArea, ?Title $xAxisLabel = null, ?Title $yAxisLabel = null, ?Axis $xAxis = null, ?Axis $yAxis = null): void { if ($plotArea === null) { return; } - $id1 = $id2 = 0; + $id1 = $id2 = $id3 = '0'; $this->seriesIndex = 0; $objWriter->startElement('c:plotArea'); @@ -218,14 +256,18 @@ class Chart extends WriterPart $groupType = $plotGroup->getPlotType(); if ($groupType == $chartType) { $plotStyle = $plotGroup->getPlotStyle(); - if ($groupType === DataSeries::TYPE_RADARCHART) { + if (!empty($plotStyle) && $groupType === DataSeries::TYPE_RADARCHART) { $objWriter->startElement('c:radarStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); - } elseif ($groupType === DataSeries::TYPE_SCATTERCHART) { + } elseif (!empty($plotStyle) && $groupType === DataSeries::TYPE_SCATTERCHART) { $objWriter->startElement('c:scatterStyle'); $objWriter->writeAttribute('val', $plotStyle); $objWriter->endElement(); + } elseif ($groupType === DataSeries::TYPE_SURFACECHART_3D || $groupType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:wireframe'); + $objWriter->writeAttribute('val', $plotStyle ? '1' : '0'); + $objWriter->endElement(); } $this->writePlotGroup($plotGroup, $chartType, $objWriter, $catIsMultiLevelSeries, $valIsMultiLevelSeries, $plotGroupingType); @@ -237,25 +279,28 @@ class Chart extends WriterPart if ($chartType === DataSeries::TYPE_LINECHART && $plotGroup) { // Line only, Line3D can't be smoothed $objWriter->startElement('c:smooth'); - $objWriter->writeAttribute('val', (int) $plotGroup->getSmoothLine()); + $objWriter->writeAttribute('val', (string) (int) $plotGroup->getSmoothLine()); $objWriter->endElement(); } elseif (($chartType === DataSeries::TYPE_BARCHART) || ($chartType === DataSeries::TYPE_BARCHART_3D)) { $objWriter->startElement('c:gapWidth'); - $objWriter->writeAttribute('val', 150); + $objWriter->writeAttribute('val', '150'); $objWriter->endElement(); if ($plotGroupingType == 'percentStacked' || $plotGroupingType == 'stacked') { $objWriter->startElement('c:overlap'); - $objWriter->writeAttribute('val', 100); + $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); } } elseif ($chartType === DataSeries::TYPE_BUBBLECHART) { - $objWriter->startElement('c:bubbleScale'); - $objWriter->writeAttribute('val', 25); - $objWriter->endElement(); + $scale = ($plotGroup === null) ? '' : (string) $plotGroup->getPlotStyle(); + if ($scale !== '') { + $objWriter->startElement('c:bubbleScale'); + $objWriter->writeAttribute('val', $scale); + $objWriter->endElement(); + } $objWriter->startElement('c:showNegBubbles'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } elseif ($chartType === DataSeries::TYPE_STOCKCHART) { $objWriter->startElement('c:hiLowLines'); @@ -264,7 +309,7 @@ class Chart extends WriterPart $objWriter->startElement('c:upDownBars'); $objWriter->startElement('c:gapWidth'); - $objWriter->writeAttribute('val', 300); + $objWriter->writeAttribute('val', '300'); $objWriter->endElement(); $objWriter->startElement('c:upBars'); @@ -276,9 +321,10 @@ class Chart extends WriterPart $objWriter->endElement(); } - // Generate 2 unique numbers to use for axId values - $id1 = '75091328'; - $id2 = '75089408'; + // Generate 3 unique numbers to use for axId values + $id1 = '110438656'; + $id2 = '110444544'; + $id3 = '110365312'; // used in Surface Chart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { $objWriter->startElement('c:axId'); @@ -287,14 +333,19 @@ class Chart extends WriterPart $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); + } } else { $objWriter->startElement('c:firstSliceAng'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); if ($chartType === DataSeries::TYPE_DONUTCHART) { $objWriter->startElement('c:holeSize'); - $objWriter->writeAttribute('val', 50); + $objWriter->writeAttribute('val', '50'); $objWriter->endElement(); } } @@ -304,15 +355,54 @@ class Chart extends WriterPart if (($chartType !== DataSeries::TYPE_PIECHART) && ($chartType !== DataSeries::TYPE_PIECHART_3D) && ($chartType !== DataSeries::TYPE_DONUTCHART)) { if ($chartType === DataSeries::TYPE_BUBBLECHART) { - $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id1, $id2, $catIsMultiLevelSeries, $xAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $xAxisLabel, $chartType, $id2, $id1, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } else { - $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis); + $this->writeCategoryAxis($objWriter, $xAxisLabel, $id1, $id2, $catIsMultiLevelSeries, $xAxis ?? new Axis()); } - $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis, $majorGridlines, $minorGridlines); + $this->writeValueAxis($objWriter, $yAxisLabel, $chartType, $id1, $id2, $valIsMultiLevelSeries, $yAxis ?? new Axis()); + if ($chartType === DataSeries::TYPE_SURFACECHART_3D || $chartType === DataSeries::TYPE_SURFACECHART) { + $this->writeSerAxis($objWriter, $id2, $id3); + } + } + $stops = $plotArea->getGradientFillStops(); + if ($plotArea->getNoFill() || !empty($stops)) { + $objWriter->startElement('c:spPr'); + if ($plotArea->getNoFill()) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); // a:noFill + } + if (!empty($stops)) { + $objWriter->startElement('a:gradFill'); + $objWriter->startElement('a:gsLst'); + foreach ($stops as $stop) { + $objWriter->startElement('a:gs'); + $objWriter->writeAttribute('pos', (string) (Properties::PERCENTAGE_MULTIPLIER * (float) $stop[0])); + $this->writeColor($objWriter, $stop[1], false); + $objWriter->endElement(); // a:gs + } + $objWriter->endElement(); // a:gsLst + $angle = $plotArea->getGradientFillAngle(); + if ($angle !== null) { + $objWriter->startElement('a:lin'); + $objWriter->writeAttribute('ang', Properties::angleToXml($angle)); + $objWriter->endElement(); // a:lin + } + $objWriter->endElement(); // a:gradFill + } + $objWriter->endElement(); // c:spPr } - $objWriter->endElement(); + $objWriter->endElement(); // c:plotArea + } + + private function writeDataLabelsBool(XMLWriter $objWriter, string $name, ?bool $value): void + { + if ($value !== null) { + $objWriter->startElement("c:$name"); + $objWriter->writeAttribute('val', $value ? '1' : '0'); + $objWriter->endElement(); + } } /** @@ -320,44 +410,72 @@ class Chart extends WriterPart */ private function writeDataLabels(XMLWriter $objWriter, ?Layout $chartLayout = null): void { + if (!isset($chartLayout)) { + return; + } $objWriter->startElement('c:dLbls'); - $objWriter->startElement('c:showLegendKey'); - $showLegendKey = (empty($chartLayout)) ? 0 : $chartLayout->getShowLegendKey(); - $objWriter->writeAttribute('val', ((empty($showLegendKey)) ? 0 : 1)); - $objWriter->endElement(); + $fillColor = $chartLayout->getLabelFillColor(); + $borderColor = $chartLayout->getLabelBorderColor(); + if ($fillColor && $fillColor->isUsable()) { + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $fillColor); + if ($borderColor && $borderColor->isUsable()) { + $objWriter->startElement('a:ln'); + $this->writeColor($objWriter, $borderColor); + $objWriter->endElement(); // a:ln + } + $objWriter->endElement(); // c:spPr + } + $fontColor = $chartLayout->getLabelFontColor(); + if ($fontColor && $fontColor->isUsable()) { + $objWriter->startElement('c:txPr'); - $objWriter->startElement('c:showVal'); - $showVal = (empty($chartLayout)) ? 0 : $chartLayout->getShowVal(); - $objWriter->writeAttribute('val', ((empty($showVal)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('wrap', 'square'); + $objWriter->writeAttribute('lIns', '38100'); + $objWriter->writeAttribute('tIns', '19050'); + $objWriter->writeAttribute('rIns', '38100'); + $objWriter->writeAttribute('bIns', '19050'); + $objWriter->writeAttribute('anchor', 'ctr'); + $objWriter->startElement('a:spAutoFit'); + $objWriter->endElement(); // a:spAutoFit + $objWriter->endElement(); // a:bodyPr - $objWriter->startElement('c:showCatName'); - $showCatName = (empty($chartLayout)) ? 0 : $chartLayout->getShowCatName(); - $objWriter->writeAttribute('val', ((empty($showCatName)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle - $objWriter->startElement('c:showSerName'); - $showSerName = (empty($chartLayout)) ? 0 : $chartLayout->getShowSerName(); - $objWriter->writeAttribute('val', ((empty($showSerName)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $this->writeColor($objWriter, $fontColor); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p - $objWriter->startElement('c:showPercent'); - $showPercent = (empty($chartLayout)) ? 0 : $chartLayout->getShowPercent(); - $objWriter->writeAttribute('val', ((empty($showPercent)) ? 0 : 1)); - $objWriter->endElement(); + $objWriter->endElement(); // c:txPr + } - $objWriter->startElement('c:showBubbleSize'); - $showBubbleSize = (empty($chartLayout)) ? 0 : $chartLayout->getShowBubbleSize(); - $objWriter->writeAttribute('val', ((empty($showBubbleSize)) ? 0 : 1)); - $objWriter->endElement(); + if ($chartLayout->getNumFmtCode() !== '') { + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', $chartLayout->getnumFmtCode()); + $objWriter->writeAttribute('sourceLinked', (string) (int) $chartLayout->getnumFmtLinked()); + $objWriter->endElement(); // c:numFmt + } + if ($chartLayout->getDLblPos() !== '') { + $objWriter->startElement('c:dLblPos'); + $objWriter->writeAttribute('val', $chartLayout->getDLblPos()); + $objWriter->endElement(); // c:dLblPos + } + $this->writeDataLabelsBool($objWriter, 'showLegendKey', $chartLayout->getShowLegendKey()); + $this->writeDataLabelsBool($objWriter, 'showVal', $chartLayout->getShowVal()); + $this->writeDataLabelsBool($objWriter, 'showCatName', $chartLayout->getShowCatName()); + $this->writeDataLabelsBool($objWriter, 'showSerName', $chartLayout->getShowSerName()); + $this->writeDataLabelsBool($objWriter, 'showPercent', $chartLayout->getShowPercent()); + $this->writeDataLabelsBool($objWriter, 'showBubbleSize', $chartLayout->getShowBubbleSize()); + $this->writeDataLabelsBool($objWriter, 'showLeaderLines', $chartLayout->getShowLeaderLines()); - $objWriter->startElement('c:showLeaderLines'); - $showLeaderLines = (empty($chartLayout)) ? 1 : $chartLayout->getShowLeaderLines(); - $objWriter->writeAttribute('val', ((empty($showLeaderLines)) ? 0 : 1)); - $objWriter->endElement(); - - $objWriter->endElement(); + $objWriter->endElement(); // c:dLbls } /** @@ -369,28 +487,69 @@ class Chart extends WriterPart */ private function writeCategoryAxis(XMLWriter $objWriter, ?Title $xAxisLabel, $id1, $id2, $isMultiLevelSeries, Axis $yAxis): void { - $objWriter->startElement('c:catAx'); + // N.B. writeCategoryAxis may be invoked with the last parameter($yAxis) using $xAxis for ScatterChart, etc + // In that case, xAxis may contain values like the yAxis, or it may be a date axis (LINECHART). + $axisType = $yAxis->getAxisType(); + if ($axisType !== '') { + $objWriter->startElement("c:$axisType"); + } elseif ($yAxis->getAxisIsNumericFormat()) { + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); + } else { + $objWriter->startElement('c:' . Axis::AXIS_TYPE_CATEGORY); + } + $majorGridlines = $yAxis->getMajorGridlines(); + $minorGridlines = $yAxis->getMinorGridlines(); - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id1); $objWriter->endElement(); } $objWriter->startElement('c:scaling'); - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); - $objWriter->endElement(); - $objWriter->endElement(); + if ($yAxis->getAxisOptionsProperty('maximum') !== null) { + $objWriter->startElement('c:max'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('maximum')); + $objWriter->endElement(); + } + if ($yAxis->getAxisOptionsProperty('minimum') !== null) { + $objWriter->startElement('c:min'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minimum')); + $objWriter->endElement(); + } + if (!empty($yAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); $objWriter->writeAttribute('val', 'b'); $objWriter->endElement(); + if ($majorGridlines !== null) { + $objWriter->startElement('c:majorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $majorGridlines); + $this->writeEffects($objWriter, $majorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end majorGridLines + } + + if ($minorGridlines !== null && $minorGridlines->getObjectState()) { + $objWriter->startElement('c:minorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $minorGridlines); + $this->writeEffects($objWriter, $minorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end minorGridLines + } + if ($xAxisLabel !== null) { $objWriter->startElement('c:title'); $objWriter->startElement('c:tx'); @@ -403,26 +562,22 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); $caption = $xAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); - $objWriter->endElement(); $layout = $xAxisLabel->getLayout(); $this->writeLayout($objWriter, $layout); $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -433,30 +588,73 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $yAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } - if ($id2 > 0) { + $textRotation = $yAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } + + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $yAxis->getFillColorObject()); + $this->writeEffects($objWriter, $yAxis); + $objWriter->endElement(); // spPr + + if ($yAxis->getAxisOptionsProperty('major_unit') !== null) { + $objWriter->startElement('c:majorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('major_unit')); + $objWriter->endElement(); + } + + if ($yAxis->getAxisOptionsProperty('minor_unit') !== null) { + $objWriter->startElement('c:minorUnit'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('minor_unit')); + $objWriter->endElement(); + } + + if ($id2 !== '0') { $objWriter->startElement('c:crossAx'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + if (!empty($yAxis->getAxisOptionsProperty('horizontal_crosses'))) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $yAxis->getAxisOptionsProperty('horizontal_crosses')); + $objWriter->endElement(); + } } $objWriter->startElement('c:auto'); - $objWriter->writeAttribute('val', 1); + // LineChart with dateAx wants '0' + $objWriter->writeAttribute('val', ($axisType === Axis::AXIS_TYPE_DATE) ? '0' : '1'); $objWriter->endElement(); $objWriter->startElement('c:lblAlgn'); @@ -464,12 +662,36 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:lblOffset'); - $objWriter->writeAttribute('val', 100); + $objWriter->writeAttribute('val', '100'); $objWriter->endElement(); + if ($axisType === Axis::AXIS_TYPE_DATE) { + $property = 'baseTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'majorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + $property = 'minorTimeUnit'; + $propertyVal = $yAxis->getAxisOptionsProperty($property); + if (!empty($propertyVal)) { + $objWriter->startElement("c:$property"); + $objWriter->writeAttribute('val', $propertyVal); + $objWriter->endElement(); + } + } + if ($isMultiLevelSeries) { $objWriter->startElement('c:noMultiLvlLbl'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } $objWriter->endElement(); @@ -483,11 +705,13 @@ class Chart extends WriterPart * @param string $id2 * @param bool $isMultiLevelSeries */ - private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis, GridLines $majorGridlines, GridLines $minorGridlines): void + private function writeValueAxis(XMLWriter $objWriter, ?Title $yAxisLabel, $groupType, $id1, $id2, $isMultiLevelSeries, Axis $xAxis): void { - $objWriter->startElement('c:valAx'); + $objWriter->startElement('c:' . Axis::AXIS_TYPE_VALUE); + $majorGridlines = $xAxis->getMajorGridlines(); + $minorGridlines = $xAxis->getMinorGridlines(); - if ($id2 > 0) { + if ($id2 !== '0') { $objWriter->startElement('c:axId'); $objWriter->writeAttribute('val', $id2); $objWriter->endElement(); @@ -507,229 +731,36 @@ class Chart extends WriterPart $objWriter->endElement(); } - $objWriter->startElement('c:orientation'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + if (!empty($xAxis->getAxisOptionsProperty('orientation'))) { + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('orientation')); + $objWriter->endElement(); + } - $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // c:scaling $objWriter->startElement('c:delete'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('hidden') ?? '0'); $objWriter->endElement(); $objWriter->startElement('c:axPos'); $objWriter->writeAttribute('val', 'l'); $objWriter->endElement(); - $objWriter->startElement('c:majorGridlines'); - $objWriter->startElement('c:spPr'); - - if ($majorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$majorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $majorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($majorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($majorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $majorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $majorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $majorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln - } - $objWriter->startElement('a:effectLst'); - - if ($majorGridlines->getGlowSize() !== null) { - $objWriter->startElement('a:glow'); - $objWriter->writeAttribute('rad', $majorGridlines->getGlowSize()); - $objWriter->startElement("a:{$majorGridlines->getGlowColor('type')}"); - $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getGlowColor('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end schemeClr - $objWriter->endElement(); //end glow + if ($majorGridlines !== null) { + $objWriter->startElement('c:majorGridlines'); + $objWriter->startElement('c:spPr'); + $this->writeLineStyles($objWriter, $majorGridlines); + $this->writeEffects($objWriter, $majorGridlines); + $objWriter->endElement(); //end spPr + $objWriter->endElement(); //end majorGridLines } - if ($majorGridlines->getShadowProperty('presets') !== null) { - $objWriter->startElement("a:{$majorGridlines->getShadowProperty('effect')}"); - if ($majorGridlines->getShadowProperty('blur') !== null) { - $objWriter->writeAttribute('blurRad', $majorGridlines->getShadowProperty('blur')); - } - if ($majorGridlines->getShadowProperty('distance') !== null) { - $objWriter->writeAttribute('dist', $majorGridlines->getShadowProperty('distance')); - } - if ($majorGridlines->getShadowProperty('direction') !== null) { - $objWriter->writeAttribute('dir', $majorGridlines->getShadowProperty('direction')); - } - if ($majorGridlines->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $majorGridlines->getShadowProperty('algn')); - } - if ($majorGridlines->getShadowProperty(['size', 'sx']) !== null) { - $objWriter->writeAttribute('sx', $majorGridlines->getShadowProperty(['size', 'sx'])); - } - if ($majorGridlines->getShadowProperty(['size', 'sy']) !== null) { - $objWriter->writeAttribute('sy', $majorGridlines->getShadowProperty(['size', 'sy'])); - } - if ($majorGridlines->getShadowProperty(['size', 'kx']) !== null) { - $objWriter->writeAttribute('kx', $majorGridlines->getShadowProperty(['size', 'kx'])); - } - if ($majorGridlines->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $majorGridlines->getShadowProperty('rotWithShape')); - } - $objWriter->startElement("a:{$majorGridlines->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'value'])); - - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $majorGridlines->getShadowProperty(['color', 'alpha'])); - $objWriter->endElement(); //end alpha - - $objWriter->endElement(); //end color:type - $objWriter->endElement(); //end shadow - } - - if ($majorGridlines->getSoftEdgesSize() !== null) { - $objWriter->startElement('a:softEdge'); - $objWriter->writeAttribute('rad', $majorGridlines->getSoftEdgesSize()); - $objWriter->endElement(); //end softEdge - } - - $objWriter->endElement(); //end effectLst - $objWriter->endElement(); //end spPr - $objWriter->endElement(); //end majorGridLines - - if ($minorGridlines->getObjectState()) { + if ($minorGridlines !== null && $minorGridlines->getObjectState()) { $objWriter->startElement('c:minorGridlines'); $objWriter->startElement('c:spPr'); - - if ($minorGridlines->getLineColorProperty('value') !== null) { - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleProperty('width')); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement("a:{$minorGridlines->getLineColorProperty('type')}"); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getLineColorProperty('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end srgbClr - $objWriter->endElement(); //end solidFill - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $minorGridlines->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($minorGridlines->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('head', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('head', 'len')); - $objWriter->endElement(); - } - - if ($minorGridlines->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $minorGridlines->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $minorGridlines->getLineStyleArrowParameters('end', 'w')); - $objWriter->writeAttribute('len', $minorGridlines->getLineStyleArrowParameters('end', 'len')); - $objWriter->endElement(); - } - $objWriter->endElement(); //end ln - } - - $objWriter->startElement('a:effectLst'); - - if ($minorGridlines->getGlowSize() !== null) { - $objWriter->startElement('a:glow'); - $objWriter->writeAttribute('rad', $minorGridlines->getGlowSize()); - $objWriter->startElement("a:{$minorGridlines->getGlowColor('type')}"); - $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getGlowColor('alpha')); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end schemeClr - $objWriter->endElement(); //end glow - } - - if ($minorGridlines->getShadowProperty('presets') !== null) { - $objWriter->startElement("a:{$minorGridlines->getShadowProperty('effect')}"); - if ($minorGridlines->getShadowProperty('blur') !== null) { - $objWriter->writeAttribute('blurRad', $minorGridlines->getShadowProperty('blur')); - } - if ($minorGridlines->getShadowProperty('distance') !== null) { - $objWriter->writeAttribute('dist', $minorGridlines->getShadowProperty('distance')); - } - if ($minorGridlines->getShadowProperty('direction') !== null) { - $objWriter->writeAttribute('dir', $minorGridlines->getShadowProperty('direction')); - } - if ($minorGridlines->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $minorGridlines->getShadowProperty('algn')); - } - if ($minorGridlines->getShadowProperty(['size', 'sx']) !== null) { - $objWriter->writeAttribute('sx', $minorGridlines->getShadowProperty(['size', 'sx'])); - } - if ($minorGridlines->getShadowProperty(['size', 'sy']) !== null) { - $objWriter->writeAttribute('sy', $minorGridlines->getShadowProperty(['size', 'sy'])); - } - if ($minorGridlines->getShadowProperty(['size', 'kx']) !== null) { - $objWriter->writeAttribute('kx', $minorGridlines->getShadowProperty(['size', 'kx'])); - } - if ($minorGridlines->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $minorGridlines->getShadowProperty('rotWithShape')); - } - $objWriter->startElement("a:{$minorGridlines->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'value'])); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $minorGridlines->getShadowProperty(['color', 'alpha'])); - $objWriter->endElement(); //end alpha - $objWriter->endElement(); //end color:type - $objWriter->endElement(); //end shadow - } - - if ($minorGridlines->getSoftEdgesSize() !== null) { - $objWriter->startElement('a:softEdge'); - $objWriter->writeAttribute('rad', $minorGridlines->getSoftEdgesSize()); - $objWriter->endElement(); //end softEdge - } - - $objWriter->endElement(); //end effectLst + $this->writeLineStyles($objWriter, $minorGridlines); + $this->writeEffects($objWriter, $minorGridlines); $objWriter->endElement(); //end spPr $objWriter->endElement(); //end minorGridLines } @@ -746,18 +777,13 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('a:p'); - $objWriter->startElement('a:r'); $caption = $yAxisLabel->getCaption(); if (is_array($caption)) { $caption = $caption[0]; } + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); - $objWriter->startElement('a:t'); - $objWriter->writeRawData(StringHelper::controlCharacterPHP2OOXML($caption)); - $objWriter->endElement(); - - $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); $objWriter->endElement(); @@ -768,7 +794,7 @@ class Chart extends WriterPart } $objWriter->startElement('c:overlay'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); $objWriter->endElement(); @@ -779,143 +805,50 @@ class Chart extends WriterPart $objWriter->writeAttribute('sourceLinked', $xAxis->getAxisNumberSourceLinked()); $objWriter->endElement(); - $objWriter->startElement('c:majorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('major_tick_mark'))) { + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('major_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:minorTickMark'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('minor_tick_mark'))) { + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('minor_tick_mark')); + $objWriter->endElement(); + } - $objWriter->startElement('c:tickLblPos'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); - $objWriter->endElement(); + if (!empty($xAxis->getAxisOptionsProperty('axis_labels'))) { + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('axis_labels')); + $objWriter->endElement(); + } + + $textRotation = $xAxis->getAxisOptionsProperty('textRotation'); + if (is_numeric($textRotation)) { + $objWriter->startElement('c:txPr'); + $objWriter->startElement('a:bodyPr'); + $objWriter->writeAttribute('rot', Properties::angleToXml((float) $textRotation)); + $objWriter->endElement(); // a:bodyPr + $objWriter->startElement('a:lstStyle'); + $objWriter->endElement(); // a:lstStyle + $objWriter->startElement('a:p'); + $objWriter->startElement('a:pPr'); + $objWriter->startElement('a:defRPr'); + $objWriter->endElement(); // a:defRPr + $objWriter->endElement(); // a:pPr + $objWriter->endElement(); // a:p + $objWriter->endElement(); // c:txPr + } $objWriter->startElement('c:spPr'); - - if ($xAxis->getFillProperty('value') !== null) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $xAxis->getFillProperty('type')); - $objWriter->writeAttribute('val', $xAxis->getFillProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getFillProperty('alpha')); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - $objWriter->startElement('a:ln'); - - $objWriter->writeAttribute('w', $xAxis->getLineStyleProperty('width')); - $objWriter->writeAttribute('cap', $xAxis->getLineStyleProperty('cap')); - $objWriter->writeAttribute('cmpd', $xAxis->getLineStyleProperty('compound')); - - if ($xAxis->getLineProperty('value') !== null) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:' . $xAxis->getLineProperty('type')); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('value')); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getLineProperty('alpha')); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - $objWriter->startElement('a:prstDash'); - $objWriter->writeAttribute('val', $xAxis->getLineStyleProperty('dash')); - $objWriter->endElement(); - - if ($xAxis->getLineStyleProperty('join') == 'miter') { - $objWriter->startElement('a:miter'); - $objWriter->writeAttribute('lim', '800000'); - $objWriter->endElement(); - } else { - $objWriter->startElement('a:bevel'); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'head', 'type']) !== null) { - $objWriter->startElement('a:headEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'head', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('head')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('head')); - $objWriter->endElement(); - } - - if ($xAxis->getLineStyleProperty(['arrow', 'end', 'type']) !== null) { - $objWriter->startElement('a:tailEnd'); - $objWriter->writeAttribute('type', $xAxis->getLineStyleProperty(['arrow', 'end', 'type'])); - $objWriter->writeAttribute('w', $xAxis->getLineStyleArrowWidth('end')); - $objWriter->writeAttribute('len', $xAxis->getLineStyleArrowLength('end')); - $objWriter->endElement(); - } - - $objWriter->endElement(); - - $objWriter->startElement('a:effectLst'); - - if ($xAxis->getGlowProperty('size') !== null) { - $objWriter->startElement('a:glow'); - $objWriter->writeAttribute('rad', $xAxis->getGlowProperty('size')); - $objWriter->startElement("a:{$xAxis->getGlowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $xAxis->getGlowProperty(['color', 'value'])); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getGlowProperty(['color', 'alpha'])); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - - if ($xAxis->getShadowProperty('presets') !== null) { - $objWriter->startElement("a:{$xAxis->getShadowProperty('effect')}"); - - if ($xAxis->getShadowProperty('blur') !== null) { - $objWriter->writeAttribute('blurRad', $xAxis->getShadowProperty('blur')); - } - if ($xAxis->getShadowProperty('distance') !== null) { - $objWriter->writeAttribute('dist', $xAxis->getShadowProperty('distance')); - } - if ($xAxis->getShadowProperty('direction') !== null) { - $objWriter->writeAttribute('dir', $xAxis->getShadowProperty('direction')); - } - if ($xAxis->getShadowProperty('algn') !== null) { - $objWriter->writeAttribute('algn', $xAxis->getShadowProperty('algn')); - } - if ($xAxis->getShadowProperty(['size', 'sx']) !== null) { - $objWriter->writeAttribute('sx', $xAxis->getShadowProperty(['size', 'sx'])); - } - if ($xAxis->getShadowProperty(['size', 'sy']) !== null) { - $objWriter->writeAttribute('sy', $xAxis->getShadowProperty(['size', 'sy'])); - } - if ($xAxis->getShadowProperty(['size', 'kx']) !== null) { - $objWriter->writeAttribute('kx', $xAxis->getShadowProperty(['size', 'kx'])); - } - if ($xAxis->getShadowProperty('rotWithShape') !== null) { - $objWriter->writeAttribute('rotWithShape', $xAxis->getShadowProperty('rotWithShape')); - } - - $objWriter->startElement("a:{$xAxis->getShadowProperty(['color', 'type'])}"); - $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'value'])); - $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $xAxis->getShadowProperty(['color', 'alpha'])); - $objWriter->endElement(); - $objWriter->endElement(); - - $objWriter->endElement(); - } - - if ($xAxis->getSoftEdgesSize() !== null) { - $objWriter->startElement('a:softEdge'); - $objWriter->writeAttribute('rad', $xAxis->getSoftEdgesSize()); - $objWriter->endElement(); - } - - $objWriter->endElement(); //effectList + $this->writeColor($objWriter, $xAxis->getFillColorObject()); + $this->writeLineStyles($objWriter, $xAxis); + $this->writeEffects($objWriter, $xAxis); $objWriter->endElement(); //end spPr - if ($id1 > 0) { + if ($id1 !== '0') { $objWriter->startElement('c:crossAx'); - $objWriter->writeAttribute('val', $id2); + $objWriter->writeAttribute('val', $id1); $objWriter->endElement(); if ($xAxis->getAxisOptionsProperty('horizontal_crosses_value') !== null) { @@ -923,14 +856,20 @@ class Chart extends WriterPart $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses_value')); $objWriter->endElement(); } else { - $objWriter->startElement('c:crosses'); - $objWriter->writeAttribute('val', $xAxis->getAxisOptionsProperty('horizontal_crosses')); - $objWriter->endElement(); + $crosses = $xAxis->getAxisOptionsProperty('horizontal_crosses'); + if ($crosses) { + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', $crosses); + $objWriter->endElement(); + } } - $objWriter->startElement('c:crossBetween'); - $objWriter->writeAttribute('val', 'midCat'); - $objWriter->endElement(); + $crossBetween = $xAxis->getCrossBetween(); + if ($crossBetween !== '') { + $objWriter->startElement('c:crossBetween'); + $objWriter->writeAttribute('val', $crossBetween); + $objWriter->endElement(); + } if ($xAxis->getAxisOptionsProperty('major_unit') !== null) { $objWriter->startElement('c:majorUnit'); @@ -948,7 +887,7 @@ class Chart extends WriterPart if ($isMultiLevelSeries) { if ($groupType !== DataSeries::TYPE_BUBBLECHART) { $objWriter->startElement('c:noMultiLvlLbl'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } } @@ -956,6 +895,54 @@ class Chart extends WriterPart $objWriter->endElement(); } + /** + * Write Ser Axis, for Surface chart. + */ + private function writeSerAxis(XMLWriter $objWriter, string $id2, string $id3): void + { + $objWriter->startElement('c:serAx'); + + $objWriter->startElement('c:axId'); + $objWriter->writeAttribute('val', $id3); + $objWriter->endElement(); // axId + + $objWriter->startElement('c:scaling'); + $objWriter->startElement('c:orientation'); + $objWriter->writeAttribute('val', 'minMax'); + $objWriter->endElement(); // orientation + $objWriter->endElement(); // scaling + + $objWriter->startElement('c:delete'); + $objWriter->writeAttribute('val', '0'); + $objWriter->endElement(); // delete + + $objWriter->startElement('c:axPos'); + $objWriter->writeAttribute('val', 'b'); + $objWriter->endElement(); // axPos + + $objWriter->startElement('c:majorTickMark'); + $objWriter->writeAttribute('val', 'out'); + $objWriter->endElement(); // majorTickMark + + $objWriter->startElement('c:minorTickMark'); + $objWriter->writeAttribute('val', 'none'); + $objWriter->endElement(); // minorTickMark + + $objWriter->startElement('c:tickLblPos'); + $objWriter->writeAttribute('val', 'nextTo'); + $objWriter->endElement(); // tickLblPos + + $objWriter->startElement('c:crossAx'); + $objWriter->writeAttribute('val', $id2); + $objWriter->endElement(); // crossAx + + $objWriter->startElement('c:crosses'); + $objWriter->writeAttribute('val', 'autoZero'); + $objWriter->endElement(); // crosses + + $objWriter->endElement(); //serAx + } + /** * Get the data series type(s) for a chart plot series. * @@ -983,29 +970,23 @@ class Chart extends WriterPart /** * Method writing plot series values. - * - * @param int $val value for idx (default: 3) - * @param string $fillColor hex color (default: FF9900) */ - private function writePlotSeriesValuesElement(XMLWriter $objWriter, $val = 3, $fillColor = 'FF9900'): void + private function writePlotSeriesValuesElement(XMLWriter $objWriter, int $val, ?ChartColor $fillColor): void { + if ($fillColor === null || !$fillColor->isUsable()) { + return; + } $objWriter->startElement('c:dPt'); - $objWriter->startElement('c:idx'); - $objWriter->writeAttribute('val', $val); - $objWriter->endElement(); - $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', 0); - $objWriter->endElement(); + $objWriter->startElement('c:idx'); + $objWriter->writeAttribute('val', "$val"); + $objWriter->endElement(); // c:idx $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); + $this->writeColor($objWriter, $fillColor); + $objWriter->endElement(); // c:spPr + + $objWriter->endElement(); // c:dPt } /** @@ -1016,7 +997,7 @@ class Chart extends WriterPart * @param bool $valIsMultiLevelSeries Is value set a multi-series set * @param string $plotGroupingType Type of grouping for multi-series values */ - private function writePlotGroup(?DataSeries $plotGroup, $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void + private function writePlotGroup(?DataSeries $plotGroup, string $groupType, XMLWriter $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void { if ($plotGroup === null) { return; @@ -1028,8 +1009,8 @@ class Chart extends WriterPart $objWriter->endElement(); } - if ($plotGroup->getPlotGrouping() !== null) { - $plotGroupingType = $plotGroup->getPlotGrouping(); + $plotGroupingType = $plotGroup->getPlotGrouping(); + if ($plotGroupingType !== null && $groupType !== DataSeries::TYPE_SURFACECHART && $groupType !== DataSeries::TYPE_SURFACECHART_3D) { $objWriter->startElement('c:grouping'); $objWriter->writeAttribute('val', $plotGroupingType); $objWriter->endElement(); @@ -1043,11 +1024,11 @@ class Chart extends WriterPart if ($groupType !== DataSeries::TYPE_LINECHART) { if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART) || ($plotSeriesCount > 1)) { $objWriter->startElement('c:varyColors'); - $objWriter->writeAttribute('val', 1); + $objWriter->writeAttribute('val', '1'); $objWriter->endElement(); } else { $objWriter->startElement('c:varyColors'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } } @@ -1057,44 +1038,46 @@ class Chart extends WriterPart foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) { $objWriter->startElement('c:ser'); - $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); - if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) { - $fillColor = $plotLabel->getFillColor(); - if ($fillColor !== null && !is_array($fillColor)) { - $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); - } - } - $objWriter->startElement('c:idx'); - $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesIdx); + $objWriter->writeAttribute('val', (string) ($this->seriesIndex + $plotSeriesIdx)); $objWriter->endElement(); $objWriter->startElement('c:order'); - $objWriter->writeAttribute('val', $this->seriesIndex + $plotSeriesRef); + $objWriter->writeAttribute('val', (string) ($this->seriesIndex + $plotSeriesRef)); $objWriter->endElement(); - // Values - $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesRef); - - if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { - $fillColorValues = $plotSeriesValues->getFillColor(); - if ($fillColorValues !== null && is_array($fillColorValues)) { - foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { - $this->writePlotSeriesValuesElement($objWriter, $dataKey, ($fillColorValues[$dataKey] ?? 'FF9900')); - } - } else { - $this->writePlotSeriesValuesElement($objWriter); + $plotLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); + $labelFill = null; + if ($plotLabel && $groupType === DataSeries::TYPE_LINECHART) { + $labelFill = $plotLabel->getFillColorObject(); + $labelFill = ($labelFill instanceof ChartColor) ? $labelFill : null; + } + if ($plotLabel && $groupType !== DataSeries::TYPE_LINECHART) { + $fillColor = $plotLabel->getFillColorObject(); + if ($fillColor !== null && !is_array($fillColor) && $fillColor->isUsable()) { + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $fillColor); + $objWriter->endElement(); // c:spPr } } + // Values + $plotSeriesValues = $plotGroup->getPlotValuesByIndex($plotSeriesIdx); + + if ($plotSeriesValues !== false && in_array($groupType, self::CUSTOM_COLOR_TYPES, true)) { + $fillColorValues = $plotSeriesValues->getFillColorObject(); + if ($fillColorValues !== null && is_array($fillColorValues)) { + foreach ($plotSeriesValues->getDataValues() as $dataKey => $dataValue) { + $this->writePlotSeriesValuesElement($objWriter, $dataKey, $fillColorValues[$dataKey] ?? null); + } + } + } + if ($plotSeriesValues !== false && $plotSeriesValues->getLabelLayout()) { + $this->writeDataLabels($objWriter, $plotSeriesValues->getLabelLayout()); + } + // Labels - $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesRef); + $plotSeriesLabel = $plotGroup->getPlotLabelByIndex($plotSeriesIdx); if ($plotSeriesLabel && ($plotSeriesLabel->getPointCount() > 0)) { $objWriter->startElement('c:tx'); $objWriter->startElement('c:strRef'); @@ -1104,44 +1087,56 @@ class Chart extends WriterPart } // Formatting for the points - if (($groupType == DataSeries::TYPE_LINECHART) || ($groupType == DataSeries::TYPE_STOCKCHART)) { - $plotLineWidth = 12700; - if ($plotSeriesValues) { - $plotLineWidth = $plotSeriesValues->getLineWidth(); - } - + if ( + $plotSeriesValues !== false + ) { $objWriter->startElement('c:spPr'); - $objWriter->startElement('a:ln'); - $objWriter->writeAttribute('w', $plotLineWidth); - if ($groupType == DataSeries::TYPE_STOCKCHART) { - $objWriter->startElement('a:noFill'); - $objWriter->endElement(); - } elseif ($plotLabel) { - $fillColor = $plotLabel->getFillColor(); - if (is_string($fillColor)) { - $objWriter->startElement('a:solidFill'); - $objWriter->startElement('a:srgbClr'); - $objWriter->writeAttribute('val', $fillColor); - $objWriter->endElement(); - $objWriter->endElement(); + $fillObject = $labelFill ?? $plotSeriesValues->getFillColorObject(); + $callLineStyles = true; + if ($fillObject instanceof ChartColor && $fillObject->isUsable()) { + if ($groupType === DataSeries::TYPE_LINECHART) { + $objWriter->startElement('a:ln'); + $callLineStyles = false; + } + $this->writeColor($objWriter, $fillObject); + if (!$callLineStyles) { + $objWriter->endElement(); // a:ln } } - $objWriter->endElement(); - $objWriter->endElement(); + $nofill = $groupType === DataSeries::TYPE_STOCKCHART || (($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) && !$plotSeriesValues->getScatterLines()); + if ($callLineStyles) { + $this->writeLineStyles($objWriter, $plotSeriesValues, $nofill); + $this->writeEffects($objWriter, $plotSeriesValues); + } + $objWriter->endElement(); // c:spPr } if ($plotSeriesValues) { $plotSeriesMarker = $plotSeriesValues->getPointMarker(); - if ($plotSeriesMarker) { + $markerFillColor = $plotSeriesValues->getMarkerFillColor(); + $fillUsed = $markerFillColor->IsUsable(); + $markerBorderColor = $plotSeriesValues->getMarkerBorderColor(); + $borderUsed = $markerBorderColor->isUsable(); + if ($plotSeriesMarker || $fillUsed || $borderUsed) { $objWriter->startElement('c:marker'); $objWriter->startElement('c:symbol'); - $objWriter->writeAttribute('val', $plotSeriesMarker); + if ($plotSeriesMarker) { + $objWriter->writeAttribute('val', $plotSeriesMarker); + } $objWriter->endElement(); if ($plotSeriesMarker !== 'none') { $objWriter->startElement('c:size'); - $objWriter->writeAttribute('val', 3); - $objWriter->endElement(); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointSize()); + $objWriter->endElement(); // c:size + $objWriter->startElement('c:spPr'); + $this->writeColor($objWriter, $markerFillColor); + if ($borderUsed) { + $objWriter->startElement('a:ln'); + $this->writeColor($objWriter, $markerBorderColor); + $objWriter->endElement(); // a:ln + } + $objWriter->endElement(); // spPr } $objWriter->endElement(); @@ -1150,23 +1145,103 @@ class Chart extends WriterPart if (($groupType === DataSeries::TYPE_BARCHART) || ($groupType === DataSeries::TYPE_BARCHART_3D) || ($groupType === DataSeries::TYPE_BUBBLECHART)) { $objWriter->startElement('c:invertIfNegative'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', '0'); $objWriter->endElement(); } + // Trendlines + if ($plotSeriesValues !== false) { + foreach ($plotSeriesValues->getTrendLines() as $trendLine) { + $trendLineType = $trendLine->getTrendLineType(); + $order = $trendLine->getOrder(); + $period = $trendLine->getPeriod(); + $dispRSqr = $trendLine->getDispRSqr(); + $dispEq = $trendLine->getDispEq(); + $forward = $trendLine->getForward(); + $backward = $trendLine->getBackward(); + $intercept = $trendLine->getIntercept(); + $name = $trendLine->getName(); + $trendLineColor = $trendLine->getLineColor(); // ChartColor + + $objWriter->startElement('c:trendline'); // N.B. lowercase 'ell' + if ($name !== '') { + $objWriter->startElement('c:name'); + $objWriter->writeRawData($name); + $objWriter->endElement(); // c:name + } + $objWriter->startElement('c:spPr'); + + if (!$trendLineColor->isUsable()) { + // use dataSeriesValues line color as a backup if $trendLineColor is null + $dsvLineColor = $plotSeriesValues->getLineColor(); + if ($dsvLineColor->isUsable()) { + $trendLine + ->getLineColor() + ->setColorProperties($dsvLineColor->getValue(), $dsvLineColor->getAlpha(), $dsvLineColor->getType()); + } + } // otherwise, hope Excel does the right thing + + $this->writeLineStyles($objWriter, $trendLine, false); // suppress noFill + + $objWriter->endElement(); // spPr + + $objWriter->startElement('c:trendlineType'); // N.B lowercase 'ell' + $objWriter->writeAttribute('val', $trendLineType); + $objWriter->endElement(); // trendlineType + if ($backward !== 0.0) { + $objWriter->startElement('c:backward'); + $objWriter->writeAttribute('val', "$backward"); + $objWriter->endElement(); // c:backward + } + if ($forward !== 0.0) { + $objWriter->startElement('c:forward'); + $objWriter->writeAttribute('val', "$forward"); + $objWriter->endElement(); // c:forward + } + if ($intercept !== 0.0) { + $objWriter->startElement('c:intercept'); + $objWriter->writeAttribute('val', "$intercept"); + $objWriter->endElement(); // c:intercept + } + if ($trendLineType == TrendLine::TRENDLINE_POLYNOMIAL) { + $objWriter->startElement('c:order'); + $objWriter->writeAttribute('val', $order); + $objWriter->endElement(); // order + } + if ($trendLineType == TrendLine::TRENDLINE_MOVING_AVG) { + $objWriter->startElement('c:period'); + $objWriter->writeAttribute('val', $period); + $objWriter->endElement(); // period + } + $objWriter->startElement('c:dispRSqr'); + $objWriter->writeAttribute('val', $dispRSqr ? '1' : '0'); + $objWriter->endElement(); + $objWriter->startElement('c:dispEq'); + $objWriter->writeAttribute('val', $dispEq ? '1' : '0'); + $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART || $groupType === DataSeries::TYPE_LINECHART) { + $objWriter->startElement('c:trendlineLbl'); + $objWriter->startElement('c:numFmt'); + $objWriter->writeAttribute('formatCode', 'General'); + $objWriter->writeAttribute('sourceLinked', '0'); + $objWriter->endElement(); // numFmt + $objWriter->endElement(); // trendlineLbl + } + + $objWriter->endElement(); // trendline + } + } // Category Labels - $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesRef); + $plotSeriesCategory = $plotGroup->getPlotCategoryByIndex($plotSeriesIdx); if ($plotSeriesCategory && ($plotSeriesCategory->getPointCount() > 0)) { $catIsMultiLevelSeries = $catIsMultiLevelSeries || $plotSeriesCategory->isMultiLevelSeries(); if (($groupType == DataSeries::TYPE_PIECHART) || ($groupType == DataSeries::TYPE_PIECHART_3D) || ($groupType == DataSeries::TYPE_DONUTCHART)) { - if ($plotGroup->getPlotStyle() !== null) { - $plotStyle = $plotGroup->getPlotStyle(); - if ($plotStyle) { - $objWriter->startElement('c:explosion'); - $objWriter->writeAttribute('val', 25); - $objWriter->endElement(); - } + $plotStyle = $plotGroup->getPlotStyle(); + if (is_numeric($plotStyle)) { + $objWriter->startElement('c:explosion'); + $objWriter->writeAttribute('val', $plotStyle); + $objWriter->endElement(); } } @@ -1176,7 +1251,14 @@ class Chart extends WriterPart $objWriter->startElement('c:cat'); } - $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + // xVals (Categories) are not always 'str' + // Test X-axis Label's Datatype to decide 'str' vs 'num' + $CategoryDatatype = $plotSeriesCategory->getDataType(); + if ($CategoryDatatype == DataSeriesValues::DATASERIES_TYPE_NUMBER) { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'num'); + } else { + $this->writePlotSeriesValues($plotSeriesCategory, $objWriter, $groupType, 'str'); + } $objWriter->endElement(); } @@ -1192,10 +1274,31 @@ class Chart extends WriterPart $this->writePlotSeriesValues($plotSeriesValues, $objWriter, $groupType, 'num'); $objWriter->endElement(); + if ($groupType === DataSeries::TYPE_SCATTERCHART && $plotGroup->getPlotStyle() === 'smoothMarker') { + $objWriter->startElement('c:smooth'); + $objWriter->writeAttribute('val', $plotSeriesValues->getSmoothLine() ? '1' : '0'); + $objWriter->endElement(); + } } if ($groupType === DataSeries::TYPE_BUBBLECHART) { - $this->writeBubbles($plotSeriesValues, $objWriter); + if (!empty($plotGroup->getPlotBubbleSizes()[$plotSeriesIdx])) { + $objWriter->startElement('c:bubbleSize'); + $this->writePlotSeriesValues( + $plotGroup->getPlotBubbleSizes()[$plotSeriesIdx], + $objWriter, + $groupType, + 'num' + ); + $objWriter->endElement(); + if ($plotSeriesValues !== false) { + $objWriter->startElement('c:bubble3D'); + $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); + $objWriter->endElement(); + } + } elseif ($plotSeriesValues !== false) { + $this->writeBubbles($plotSeriesValues, $objWriter); + } } $objWriter->endElement(); @@ -1219,7 +1322,7 @@ class Chart extends WriterPart $objWriter->startElement('c:strCache'); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesLabel->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesLabel->getPointCount()); $objWriter->endElement(); foreach ($plotSeriesLabel->getDataValues() as $plotLabelKey => $plotLabelValue) { @@ -1258,7 +1361,7 @@ class Chart extends WriterPart $objWriter->startElement('c:multiLvlStrCache'); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); for ($level = 0; $level < $levelCount; ++$level) { @@ -1289,41 +1392,54 @@ class Chart extends WriterPart $objWriter->writeRawData($plotSeriesValues->getDataSource()); $objWriter->endElement(); - $objWriter->startElement('c:' . $dataType . 'Cache'); + $count = $plotSeriesValues->getPointCount(); + $source = $plotSeriesValues->getDataSource(); + $values = $plotSeriesValues->getDataValues(); + if ($count > 1 || ($count === 1 && "=$source" !== (string) $values[0])) { + $objWriter->startElement('c:' . $dataType . 'Cache'); - if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) { - if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) { - $objWriter->startElement('c:formatCode'); - $objWriter->writeRawData($plotSeriesValues->getFormatCode()); - $objWriter->endElement(); - } - } - - $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); - $objWriter->endElement(); - - $dataValues = $plotSeriesValues->getDataValues(); - if (!empty($dataValues)) { - if (is_array($dataValues)) { - foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { - $objWriter->startElement('c:pt'); - $objWriter->writeAttribute('idx', $plotSeriesKey); - - $objWriter->startElement('c:v'); - $objWriter->writeRawData($plotSeriesValue); - $objWriter->endElement(); + if (($groupType != DataSeries::TYPE_PIECHART) && ($groupType != DataSeries::TYPE_PIECHART_3D) && ($groupType != DataSeries::TYPE_DONUTCHART)) { + if (($plotSeriesValues->getFormatCode() !== null) && ($plotSeriesValues->getFormatCode() !== '')) { + $objWriter->startElement('c:formatCode'); + $objWriter->writeRawData($plotSeriesValues->getFormatCode()); $objWriter->endElement(); } } + + $objWriter->startElement('c:ptCount'); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); + $objWriter->endElement(); + + $dataValues = $plotSeriesValues->getDataValues(); + if (!empty($dataValues)) { + if (is_array($dataValues)) { + foreach ($dataValues as $plotSeriesKey => $plotSeriesValue) { + $objWriter->startElement('c:pt'); + $objWriter->writeAttribute('idx', $plotSeriesKey); + + $objWriter->startElement('c:v'); + $objWriter->writeRawData($plotSeriesValue); + $objWriter->endElement(); + $objWriter->endElement(); + } + } + } + + $objWriter->endElement(); // *Cache } - $objWriter->endElement(); - - $objWriter->endElement(); + $objWriter->endElement(); // *Ref } } + private const CUSTOM_COLOR_TYPES = [ + DataSeries::TYPE_BARCHART, + DataSeries::TYPE_BARCHART_3D, + DataSeries::TYPE_PIECHART, + DataSeries::TYPE_PIECHART_3D, + DataSeries::TYPE_DONUTCHART, + ]; + /** * Write Bubble Chart Details. */ @@ -1341,7 +1457,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:ptCount'); - $objWriter->writeAttribute('val', $plotSeriesValues->getPointCount()); + $objWriter->writeAttribute('val', (string) $plotSeriesValues->getPointCount()); $objWriter->endElement(); $dataValues = $plotSeriesValues->getDataValues(); @@ -1351,7 +1467,7 @@ class Chart extends WriterPart $objWriter->startElement('c:pt'); $objWriter->writeAttribute('idx', $plotSeriesKey); $objWriter->startElement('c:v'); - $objWriter->writeRawData(1); + $objWriter->writeRawData('1'); $objWriter->endElement(); $objWriter->endElement(); } @@ -1362,7 +1478,7 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:bubble3D'); - $objWriter->writeAttribute('val', 0); + $objWriter->writeAttribute('val', $plotSeriesValues->getBubble3D() ? '1' : '0'); $objWriter->endElement(); } @@ -1400,28 +1516,28 @@ class Chart extends WriterPart $x = $layout->getXPosition(); if ($x !== null) { $objWriter->startElement('c:x'); - $objWriter->writeAttribute('val', $x); + $objWriter->writeAttribute('val', "$x"); $objWriter->endElement(); } $y = $layout->getYPosition(); if ($y !== null) { $objWriter->startElement('c:y'); - $objWriter->writeAttribute('val', $y); + $objWriter->writeAttribute('val', "$y"); $objWriter->endElement(); } $w = $layout->getWidth(); if ($w !== null) { $objWriter->startElement('c:w'); - $objWriter->writeAttribute('val', $w); + $objWriter->writeAttribute('val', "$w"); $objWriter->endElement(); } $h = $layout->getHeight(); if ($h !== null) { $objWriter->startElement('c:h'); - $objWriter->writeAttribute('val', $h); + $objWriter->writeAttribute('val', "$h"); $objWriter->endElement(); } @@ -1440,8 +1556,8 @@ class Chart extends WriterPart $objWriter->writeAttribute('xmlns:mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'); $objWriter->startElement('mc:Choice'); - $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'); $objWriter->writeAttribute('Requires', 'c14'); + $objWriter->writeAttribute('xmlns:c14', 'http://schemas.microsoft.com/office/drawing/2007/8/2/chart'); $objWriter->startElement('c14:style'); $objWriter->writeAttribute('val', '102'); @@ -1468,12 +1584,12 @@ class Chart extends WriterPart $objWriter->endElement(); $objWriter->startElement('c:pageMargins'); - $objWriter->writeAttribute('footer', 0.3); - $objWriter->writeAttribute('header', 0.3); - $objWriter->writeAttribute('r', 0.7); - $objWriter->writeAttribute('l', 0.7); - $objWriter->writeAttribute('t', 0.75); - $objWriter->writeAttribute('b', 0.75); + $objWriter->writeAttribute('footer', '0.3'); + $objWriter->writeAttribute('header', '0.3'); + $objWriter->writeAttribute('r', '0.7'); + $objWriter->writeAttribute('l', '0.7'); + $objWriter->writeAttribute('t', '0.75'); + $objWriter->writeAttribute('b', '0.75'); $objWriter->endElement(); $objWriter->startElement('c:pageSetup'); @@ -1482,4 +1598,177 @@ class Chart extends WriterPart $objWriter->endElement(); } + + private function writeEffects(XMLWriter $objWriter, Properties $yAxis): void + { + if ( + !empty($yAxis->getSoftEdgesSize()) + || !empty($yAxis->getShadowProperty('effect')) + || !empty($yAxis->getGlowProperty('size')) + ) { + $objWriter->startElement('a:effectLst'); + $this->writeGlow($objWriter, $yAxis); + $this->writeShadow($objWriter, $yAxis); + $this->writeSoftEdge($objWriter, $yAxis); + $objWriter->endElement(); // effectLst + } + } + + private function writeShadow(XMLWriter $objWriter, Properties $xAxis): void + { + if (empty($xAxis->getShadowProperty('effect'))) { + return; + } + /** @var string */ + $effect = $xAxis->getShadowProperty('effect'); + $objWriter->startElement("a:$effect"); + + if (is_numeric($xAxis->getShadowProperty('blur'))) { + $objWriter->writeAttribute('blurRad', Properties::pointsToXml((float) $xAxis->getShadowProperty('blur'))); + } + if (is_numeric($xAxis->getShadowProperty('distance'))) { + $objWriter->writeAttribute('dist', Properties::pointsToXml((float) $xAxis->getShadowProperty('distance'))); + } + if (is_numeric($xAxis->getShadowProperty('direction'))) { + $objWriter->writeAttribute('dir', Properties::angleToXml((float) $xAxis->getShadowProperty('direction'))); + } + $algn = $xAxis->getShadowProperty('algn'); + if (is_string($algn) && $algn !== '') { + $objWriter->writeAttribute('algn', $algn); + } + foreach (['sx', 'sy'] as $sizeType) { + $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]); + if (is_numeric($sizeValue)) { + $objWriter->writeAttribute($sizeType, Properties::tenthOfPercentToXml((float) $sizeValue)); + } + } + foreach (['kx', 'ky'] as $sizeType) { + $sizeValue = $xAxis->getShadowProperty(['size', $sizeType]); + if (is_numeric($sizeValue)) { + $objWriter->writeAttribute($sizeType, Properties::angleToXml((float) $sizeValue)); + } + } + $rotWithShape = $xAxis->getShadowProperty('rotWithShape'); + if (is_numeric($rotWithShape)) { + $objWriter->writeAttribute('rotWithShape', (string) (int) $rotWithShape); + } + + $this->writeColor($objWriter, $xAxis->getShadowColorObject(), false); + + $objWriter->endElement(); + } + + private function writeGlow(XMLWriter $objWriter, Properties $yAxis): void + { + $size = $yAxis->getGlowProperty('size'); + if (empty($size)) { + return; + } + $objWriter->startElement('a:glow'); + $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $size)); + $this->writeColor($objWriter, $yAxis->getGlowColorObject(), false); + $objWriter->endElement(); // glow + } + + private function writeSoftEdge(XMLWriter $objWriter, Properties $yAxis): void + { + $softEdgeSize = $yAxis->getSoftEdgesSize(); + if (empty($softEdgeSize)) { + return; + } + $objWriter->startElement('a:softEdge'); + $objWriter->writeAttribute('rad', Properties::pointsToXml((float) $softEdgeSize)); + $objWriter->endElement(); //end softEdge + } + + private function writeLineStyles(XMLWriter $objWriter, Properties $gridlines, bool $noFill = false): void + { + $objWriter->startElement('a:ln'); + $widthTemp = $gridlines->getLineStyleProperty('width'); + if (is_numeric($widthTemp)) { + $objWriter->writeAttribute('w', Properties::pointsToXml((float) $widthTemp)); + } + $this->writeNotEmpty($objWriter, 'cap', $gridlines->getLineStyleProperty('cap')); + $this->writeNotEmpty($objWriter, 'cmpd', $gridlines->getLineStyleProperty('compound')); + if ($noFill) { + $objWriter->startElement('a:noFill'); + $objWriter->endElement(); + } else { + $this->writeColor($objWriter, $gridlines->getLineColor()); + } + + $dash = $gridlines->getLineStyleProperty('dash'); + if (!empty($dash)) { + $objWriter->startElement('a:prstDash'); + $this->writeNotEmpty($objWriter, 'val', $dash); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty('join') === 'miter') { + $objWriter->startElement('a:miter'); + $objWriter->writeAttribute('lim', '800000'); + $objWriter->endElement(); + } elseif ($gridlines->getLineStyleProperty('join') === 'bevel') { + $objWriter->startElement('a:bevel'); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'head', 'type'])) { + $objWriter->startElement('a:headEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'head', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('head')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('head')); + $objWriter->endElement(); + } + + if ($gridlines->getLineStyleProperty(['arrow', 'end', 'type'])) { + $objWriter->startElement('a:tailEnd'); + $objWriter->writeAttribute('type', $gridlines->getLineStyleProperty(['arrow', 'end', 'type'])); + $this->writeNotEmpty($objWriter, 'w', $gridlines->getLineStyleArrowWidth('end')); + $this->writeNotEmpty($objWriter, 'len', $gridlines->getLineStyleArrowLength('end')); + $objWriter->endElement(); + } + $objWriter->endElement(); //end ln + } + + private function writeNotEmpty(XMLWriter $objWriter, string $name, ?string $value): void + { + if ($value !== null && $value !== '') { + $objWriter->writeAttribute($name, $value); + } + } + + private function writeColor(XMLWriter $objWriter, ChartColor $chartColor, bool $solidFill = true): void + { + $type = $chartColor->getType(); + $value = $chartColor->getValue(); + if (!empty($type) && !empty($value)) { + if ($solidFill) { + $objWriter->startElement('a:solidFill'); + } + $objWriter->startElement("a:$type"); + $objWriter->writeAttribute('val', $value); + $alpha = $chartColor->getAlpha(); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); + $objWriter->endElement(); // a:alpha + } + $brightness = $chartColor->getBrightness(); + if (is_numeric($brightness)) { + $brightness = (int) $brightness; + $lumOff = 100 - $brightness; + $objWriter->startElement('a:lumMod'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($brightness)); + $objWriter->endElement(); // a:lumMod + $objWriter->startElement('a:lumOff'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml($lumOff)); + $objWriter->endElement(); // a:lumOff + } + $objWriter->endElement(); //a:srgbClr/schemeClr/prstClr + if ($solidFill) { + $objWriter->endElement(); //a:solidFill + } + } + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php index ea0f1faa69f..5045e8f38fa 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Comments.php @@ -165,7 +165,7 @@ class Comments extends WriterPart // Metadata [$column, $row] = Coordinate::indexesFromString($cellReference); $id = 1024 + $column + $row; - $id = substr($id, 0, 4); + $id = substr("$id", 0, 4); // v:shape $objWriter->startElement('v:shape'); @@ -223,10 +223,10 @@ class Comments extends WriterPart $objWriter->writeElement('x:AutoFill', 'False'); // x:Row - $objWriter->writeElement('x:Row', ($row - 1)); + $objWriter->writeElement('x:Row', (string) ($row - 1)); // x:Column - $objWriter->writeElement('x:Column', ($column - 1)); + $objWriter->writeElement('x:Column', (string) ($column - 1)); $objWriter->endElement(); diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php index f62c14af70e..acb85b5788f 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php @@ -85,6 +85,16 @@ class ContentTypes extends WriterPart // Shared strings $this->writeOverrideContentType($objWriter, '/xl/sharedStrings.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml'); + // Table + $table = 1; + for ($i = 0; $i < $sheetCount; ++$i) { + $tableCount = $spreadsheet->getSheet($i)->getTableCollection()->count(); + + for ($t = 1; $t <= $tableCount; ++$t) { + $this->writeOverrideContentType($objWriter, '/xl/tables/table' . $table++ . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml'); + } + } + // Add worksheet relationship content types $unparsedLoadedData = $spreadsheet->getUnparsedLoadedData(); $chart = 1; diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index b8285fcbead..b3338c07b82 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -118,7 +118,7 @@ class DefinedNames $range[1] = Coordinate::absoluteCoordinate($range[1]); $range = implode(':', $range); - $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle() ?? '') . '\'!' . $range); + $this->objWriter->writeRawData('\'' . str_replace("'", "''", $worksheet->getTitle()) . '\'!' . $range); $this->objWriter->endElement(); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php index 43ce442fee7..cb8758c2bd8 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/DocProps.php @@ -56,7 +56,7 @@ class DocProps extends WriterPart // Variant $objWriter->startElement('vt:variant'); - $objWriter->writeElement('vt:i4', $spreadsheet->getSheetCount()); + $objWriter->writeElement('vt:i4', (string) $spreadsheet->getSheetCount()); $objWriter->endElement(); $objWriter->endElement(); @@ -68,7 +68,7 @@ class DocProps extends WriterPart // Vector $objWriter->startElement('vt:vector'); - $objWriter->writeAttribute('size', $spreadsheet->getSheetCount()); + $objWriter->writeAttribute('size', (string) $spreadsheet->getSheetCount()); $objWriter->writeAttribute('baseType', 'lpstr'); $sheetCount = $spreadsheet->getSheetCount(); @@ -207,7 +207,7 @@ class DocProps extends WriterPart $objWriter->startElement('property'); $objWriter->writeAttribute('fmtid', '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}'); - $objWriter->writeAttribute('pid', $key + 2); + $objWriter->writeAttribute('pid', (string) ($key + 2)); $objWriter->writeAttribute('name', $customProperty); switch ($propertyType) { diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index fa77e2d7e63..7693c72ce24 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; @@ -56,7 +57,10 @@ class Drawing extends WriterPart // Loop through charts and write the chart position if ($chartCount > 0) { for ($c = 0; $c < $chartCount; ++$c) { - $this->writeChart($objWriter, $worksheet->getChartByIndex($c), $c + $i); + $chart = $worksheet->getChartByIndex((string) $c); + if ($chart !== false) { + $this->writeChart($objWriter, $chart, $c + $i); + } } } } @@ -85,29 +89,56 @@ class Drawing extends WriterPart $tl = $chart->getTopLeftPosition(); $tlColRow = Coordinate::indexesFromString($tl['cell']); $br = $chart->getBottomRightPosition(); - $brColRow = Coordinate::indexesFromString($br['cell']); - $objWriter->startElement('xdr:twoCellAnchor'); + $isTwoCellAnchor = $br['cell'] !== ''; + if ($isTwoCellAnchor) { + $brColRow = Coordinate::indexesFromString($br['cell']); - $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $tlColRow[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset'])); - $objWriter->writeElement('xdr:row', $tlColRow[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['yOffset'])); - $objWriter->endElement(); - $objWriter->startElement('xdr:to'); - $objWriter->writeElement('xdr:col', $brColRow[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset'])); - $objWriter->writeElement('xdr:row', $brColRow[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset'])); - $objWriter->endElement(); + $objWriter->startElement('xdr:twoCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:to'); + $objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } elseif ($chart->getOneCellAnchor()) { + $objWriter->startElement('xdr:oneCellAnchor'); + + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset'])); + $objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset'])); + $objWriter->endElement(); + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset'])); + $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } else { + $objWriter->startElement('xdr:absoluteAnchor'); + $objWriter->startElement('xdr:pos'); + $objWriter->writeAttribute('x', '0'); + $objWriter->writeAttribute('y', '0'); + $objWriter->endElement(); + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($br['xOffset'])); + $objWriter->writeAttribute('cy', self::stringEmu($br['yOffset'])); + $objWriter->endElement(); + } $objWriter->startElement('xdr:graphicFrame'); $objWriter->writeAttribute('macro', ''); $objWriter->startElement('xdr:nvGraphicFramePr'); $objWriter->startElement('xdr:cNvPr'); $objWriter->writeAttribute('name', 'Chart ' . $relationId); - $objWriter->writeAttribute('id', 1025 * $relationId); + $objWriter->writeAttribute('id', (string) (1025 * $relationId)); $objWriter->endElement(); $objWriter->startElement('xdr:cNvGraphicFramePr'); $objWriter->startElement('a:graphicFrameLocks'); @@ -153,24 +184,52 @@ class Drawing extends WriterPart public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relationId = -1, $hlinkClickId = null): void { if ($relationId >= 0) { - // xdr:oneCellAnchor - $objWriter->startElement('xdr:oneCellAnchor'); - // Image location - $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates()); + $isTwoCellAnchor = $drawing->getCoordinates2() !== ''; + if ($isTwoCellAnchor) { + // xdr:twoCellAnchor + $objWriter->startElement('xdr:twoCellAnchor'); + if ($drawing->validEditAs()) { + $objWriter->writeAttribute('editAs', $drawing->getEditAs()); + } + // Image location + $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates()); + $aCoordinates2 = Coordinate::indexesFromString($drawing->getCoordinates2()); - // xdr:from - $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', $aCoordinates[0] - 1); - $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX())); - $objWriter->writeElement('xdr:row', $aCoordinates[1] - 1); - $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY())); - $objWriter->endElement(); + // xdr:from + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY())); + $objWriter->endElement(); - // xdr:ext - $objWriter->startElement('xdr:ext'); - $objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth())); - $objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight())); - $objWriter->endElement(); + // xdr:to + $objWriter->startElement('xdr:to'); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates2[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX2())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates2[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY2())); + $objWriter->endElement(); + } else { + // xdr:oneCellAnchor + $objWriter->startElement('xdr:oneCellAnchor'); + // Image location + $aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates()); + + // xdr:from + $objWriter->startElement('xdr:from'); + $objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1)); + $objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX())); + $objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1)); + $objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY())); + $objWriter->endElement(); + + // xdr:ext + $objWriter->startElement('xdr:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); + $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight())); + $objWriter->endElement(); + } // xdr:pic $objWriter->startElement('xdr:pic'); @@ -180,7 +239,7 @@ class Drawing extends WriterPart // xdr:cNvPr $objWriter->startElement('xdr:cNvPr'); - $objWriter->writeAttribute('id', $relationId); + $objWriter->writeAttribute('id', (string) $relationId); $objWriter->writeAttribute('name', $drawing->getName()); $objWriter->writeAttribute('descr', $drawing->getDescription()); @@ -222,7 +281,13 @@ class Drawing extends WriterPart // a:xfrm $objWriter->startElement('a:xfrm'); - $objWriter->writeAttribute('rot', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getRotation())); + $objWriter->writeAttribute('rot', (string) SharedDrawing::degreesToAngle($drawing->getRotation())); + if ($isTwoCellAnchor) { + $objWriter->startElement('a:ext'); + $objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth())); + $objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight())); + $objWriter->endElement(); + } $objWriter->endElement(); // a:prstGeom @@ -240,9 +305,9 @@ class Drawing extends WriterPart // a:outerShdw $objWriter->startElement('a:outerShdw'); - $objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getBlurRadius())); - $objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getDistance())); - $objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getShadow()->getDirection())); + $objWriter->writeAttribute('blurRad', self::stringEmu($drawing->getShadow()->getBlurRadius())); + $objWriter->writeAttribute('dist', self::stringEmu($drawing->getShadow()->getDistance())); + $objWriter->writeAttribute('dir', (string) SharedDrawing::degreesToAngle($drawing->getShadow()->getDirection())); $objWriter->writeAttribute('algn', $drawing->getShadow()->getAlignment()); $objWriter->writeAttribute('rotWithShape', '0'); @@ -252,7 +317,7 @@ class Drawing extends WriterPart // a:alpha $objWriter->startElement('a:alpha'); - $objWriter->writeAttribute('val', $drawing->getShadow()->getAlpha() * 1000); + $objWriter->writeAttribute('val', (string) ($drawing->getShadow()->getAlpha() * 1000)); $objWriter->endElement(); $objWriter->endElement(); @@ -497,4 +562,9 @@ class Drawing extends WriterPart $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId); $objWriter->endElement(); } + + private static function stringEmu(int $pixelValue): string + { + return (string) SharedDrawing::pixelsToEMU($pixelValue); + } } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 5aa878760ab..99fa2d34aaa 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -67,12 +67,13 @@ class Rels extends WriterPart 'xl/workbook.xml' ); // a custom UI in workbook ? + $target = $spreadsheet->getRibbonXMLData('target'); if ($spreadsheet->hasRibbon()) { $this->writeRelationShip( $objWriter, 5, 'http://schemas.microsoft.com/office/2006/relationships/ui/extensibility', - $spreadsheet->getRibbonXMLData('target') + is_string($target) ? $target : '' ); } @@ -163,10 +164,11 @@ class Rels extends WriterPart * * @param int $worksheetId * @param bool $includeCharts Flag indicating if we should write charts + * @param int $tableRef Table ID * * @return string XML Output */ - public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false) + public function writeWorksheetRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, $worksheetId = 1, $includeCharts = false, $tableRef = 1) { // Create XML writer $objWriter = null; @@ -252,6 +254,17 @@ class Rels extends WriterPart ); } + // Write Table + $tableCount = $worksheet->getTableCollection()->count(); + for ($i = 1; $i <= $tableCount; ++$i) { + $this->writeRelationship( + $objWriter, + '_table_' . $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/table', + '../tables/table' . $tableRef++ . '.xml' + ); + } + // Write header/footer relationship? $i = 1; if (count($worksheet->getHeaderFooter()->getImages()) > 0) { @@ -272,7 +285,7 @@ class Rels extends WriterPart return $objWriter->getData(); } - private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, $relationship, $type): void + private function writeUnparsedRelationship(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $worksheet, XMLWriter $objWriter, string $relationship, string $type): void { $unparsedLoadedData = $worksheet->getParent()->getUnparsedLoadedData(); if (!isset($unparsedLoadedData['sheets'][$worksheet->getCodeName()][$relationship])) { @@ -436,7 +449,7 @@ class Rels extends WriterPart /** * Write Override content type. * - * @param int $id Relationship ID. rId will be prepended! + * @param int|string $id Relationship ID. rId will be prepended! * @param string $type Relationship type * @param string $target Relationship target * @param string $targetMode Relationship target mode diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php index f9a2e711cea..078f940accc 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/StringTable.php @@ -2,7 +2,9 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\DataType; +use PhpOffice\PhpSpreadsheet\Chart\ChartColor; use PhpOffice\PhpSpreadsheet\RichText\RichText; use PhpOffice\PhpSpreadsheet\RichText\Run; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -35,8 +37,9 @@ class StringTable extends WriterPart $aFlippedStringTable = $this->flipStringTable($aStringTable); // Loop through cells - foreach ($worksheet->getCoordinates() as $coordinate) { - $cell = $worksheet->getCell($coordinate); + foreach ($worksheet->getCellCollection()->getCoordinates() as $coordinate) { + /** @var Cell $cell */ + $cell = $worksheet->getCellCollection()->get($coordinate); $cellValue = $cell->getValue(); if ( !is_object($cellValue) && @@ -63,7 +66,7 @@ class StringTable extends WriterPart /** * Write string table to XML format. * - * @param string[] $stringTable + * @param (string|RichText)[] $stringTable * * @return string XML Output */ @@ -83,13 +86,13 @@ class StringTable extends WriterPart // String table $objWriter->startElement('sst'); $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); - $objWriter->writeAttribute('uniqueCount', count($stringTable)); + $objWriter->writeAttribute('uniqueCount', (string) count($stringTable)); // Loop through string table foreach ($stringTable as $textElement) { $objWriter->startElement('si'); - if (!$textElement instanceof RichText) { + if (!($textElement instanceof RichText)) { $textToWrite = StringHelper::controlCharacterPHP2OOXML($textElement); $objWriter->startElement('t'); if ($textToWrite !== trim($textToWrite)) { @@ -97,7 +100,7 @@ class StringTable extends WriterPart } $objWriter->writeRawData($textToWrite); $objWriter->endElement(); - } elseif ($textElement instanceof RichText) { + } else { $this->writeRichText($objWriter, $textElement); } @@ -127,14 +130,16 @@ class StringTable extends WriterPart $objWriter->startElement($prefix . 'r'); // rPr - if ($element instanceof Run) { + if ($element instanceof Run && $element->getFont() !== null) { // rPr $objWriter->startElement($prefix . 'rPr'); // rFont - $objWriter->startElement($prefix . 'rFont'); - $objWriter->writeAttribute('val', $element->getFont()->getName()); - $objWriter->endElement(); + if ($element->getFont()->getName() !== null) { + $objWriter->startElement($prefix . 'rFont'); + $objWriter->writeAttribute('val', $element->getFont()->getName()); + $objWriter->endElement(); + } // Bold $objWriter->startElement($prefix . 'b'); @@ -163,19 +168,25 @@ class StringTable extends WriterPart $objWriter->endElement(); // Color - $objWriter->startElement($prefix . 'color'); - $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); - $objWriter->endElement(); + if ($element->getFont()->getColor()->getARGB() !== null) { + $objWriter->startElement($prefix . 'color'); + $objWriter->writeAttribute('rgb', $element->getFont()->getColor()->getARGB()); + $objWriter->endElement(); + } // Size - $objWriter->startElement($prefix . 'sz'); - $objWriter->writeAttribute('val', $element->getFont()->getSize()); - $objWriter->endElement(); + if ($element->getFont()->getSize() !== null) { + $objWriter->startElement($prefix . 'sz'); + $objWriter->writeAttribute('val', (string) $element->getFont()->getSize()); + $objWriter->endElement(); + } // Underline - $objWriter->startElement($prefix . 'u'); - $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); - $objWriter->endElement(); + if ($element->getFont()->getUnderline() !== null) { + $objWriter->startElement($prefix . 'u'); + $objWriter->writeAttribute('val', $element->getFont()->getUnderline()); + $objWriter->endElement(); + } $objWriter->endElement(); } @@ -196,15 +207,16 @@ class StringTable extends WriterPart * @param RichText|string $richText text string or Rich text * @param string $prefix Optional Namespace prefix */ - public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = null): void + public function writeRichTextForCharts(XMLWriter $objWriter, $richText = null, $prefix = ''): void { - if (!$richText instanceof RichText) { + if (!($richText instanceof RichText)) { $textRun = $richText; $richText = new RichText(); - $richText->createTextRun($textRun); + $run = $richText->createTextRun($textRun ?? ''); + $run->setFont(null); } - if ($prefix !== null) { + if ($prefix !== '') { $prefix .= ':'; } @@ -213,36 +225,65 @@ class StringTable extends WriterPart foreach ($elements as $element) { // r $objWriter->startElement($prefix . 'r'); + if ($element->getFont() !== null) { + // rPr + $objWriter->startElement($prefix . 'rPr'); + $size = $element->getFont()->getSize(); + if (is_numeric($size)) { + $objWriter->writeAttribute('sz', (string) (int) ($size * 100)); + } - // rPr - $objWriter->startElement($prefix . 'rPr'); + // Bold + $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? '1' : '0')); + // Italic + $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? '1' : '0')); + // Underline + $underlineType = $element->getFont()->getUnderline(); + switch ($underlineType) { + case 'single': + $underlineType = 'sng'; - // Bold - $objWriter->writeAttribute('b', ($element->getFont()->getBold() ? 1 : 0)); - // Italic - $objWriter->writeAttribute('i', ($element->getFont()->getItalic() ? 1 : 0)); - // Underline - $underlineType = $element->getFont()->getUnderline(); - switch ($underlineType) { - case 'single': - $underlineType = 'sng'; + break; + case 'double': + $underlineType = 'dbl'; - break; - case 'double': - $underlineType = 'dbl'; + break; + } + if ($underlineType !== null) { + $objWriter->writeAttribute('u', $underlineType); + } + // Strikethrough + $objWriter->writeAttribute('strike', ($element->getFont()->getStriketype() ?: 'noStrike')); + // Superscript/subscript + if ($element->getFont()->getBaseLine()) { + $objWriter->writeAttribute('baseline', (string) $element->getFont()->getBaseLine()); + } - break; + // Color + $this->writeChartTextColor($objWriter, $element->getFont()->getChartColor(), $prefix); + + // Underscore Color + $this->writeChartTextColor($objWriter, $element->getFont()->getUnderlineColor(), $prefix, 'uFill'); + + // fontName + if ($element->getFont()->getLatin()) { + $objWriter->startElement($prefix . 'latin'); + $objWriter->writeAttribute('typeface', $element->getFont()->getLatin()); + $objWriter->endElement(); + } + if ($element->getFont()->getEastAsian()) { + $objWriter->startElement($prefix . 'ea'); + $objWriter->writeAttribute('typeface', $element->getFont()->getEastAsian()); + $objWriter->endElement(); + } + if ($element->getFont()->getComplexScript()) { + $objWriter->startElement($prefix . 'cs'); + $objWriter->writeAttribute('typeface', $element->getFont()->getComplexScript()); + $objWriter->endElement(); + } + + $objWriter->endElement(); } - $objWriter->writeAttribute('u', $underlineType); - // Strikethrough - $objWriter->writeAttribute('strike', ($element->getFont()->getStrikethrough() ? 'sngStrike' : 'noStrike')); - - // rFont - $objWriter->startElement($prefix . 'latin'); - $objWriter->writeAttribute('typeface', $element->getFont()->getName()); - $objWriter->endElement(); - - $objWriter->endElement(); // t $objWriter->startElement($prefix . 't'); @@ -253,6 +294,33 @@ class StringTable extends WriterPart } } + private function writeChartTextColor(XMLWriter $objWriter, ?ChartColor $underlineColor, string $prefix, ?string $openTag = ''): void + { + if ($underlineColor !== null) { + $type = $underlineColor->getType(); + $value = $underlineColor->getValue(); + if (!empty($type) && !empty($value)) { + if ($openTag !== '') { + $objWriter->startElement($prefix . $openTag); + } + $objWriter->startElement($prefix . 'solidFill'); + $objWriter->startElement($prefix . $type); + $objWriter->writeAttribute('val', $value); + $alpha = $underlineColor->getAlpha(); + if (is_numeric($alpha)) { + $objWriter->startElement('a:alpha'); + $objWriter->writeAttribute('val', ChartColor::alphaToXml((int) $alpha)); + $objWriter->endElement(); + } + $objWriter->endElement(); // srgbClr/schemeClr/prstClr + $objWriter->endElement(); // solidFill + if ($openTag !== '') { + $objWriter->endElement(); // uFill + } + } + } + } + /** * Flip string table (for index searching). * diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php index cb2e3850477..0442c25d400 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Style.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Alignment; use PhpOffice\PhpSpreadsheet\Style\Border; use PhpOffice\PhpSpreadsheet\Style\Borders; use PhpOffice\PhpSpreadsheet\Style\Conditional; @@ -40,7 +41,7 @@ class Style extends WriterPart // numFmts $objWriter->startElement('numFmts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getNumFmtHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getNumFmtHashTable()->count()); // numFmt for ($i = 0; $i < $this->getParentWriter()->getNumFmtHashTable()->count(); ++$i) { @@ -51,54 +52,63 @@ class Style extends WriterPart // fonts $objWriter->startElement('fonts'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFontHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFontHashTable()->count()); // font for ($i = 0; $i < $this->getParentWriter()->getFontHashTable()->count(); ++$i) { - $this->writeFont($objWriter, $this->getParentWriter()->getFontHashTable()->getByIndex($i)); + $thisfont = $this->getParentWriter()->getFontHashTable()->getByIndex($i); + if ($thisfont !== null) { + $this->writeFont($objWriter, $thisfont); + } } $objWriter->endElement(); // fills $objWriter->startElement('fills'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getFillHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getFillHashTable()->count()); // fill for ($i = 0; $i < $this->getParentWriter()->getFillHashTable()->count(); ++$i) { - $this->writeFill($objWriter, $this->getParentWriter()->getFillHashTable()->getByIndex($i)); + $thisfill = $this->getParentWriter()->getFillHashTable()->getByIndex($i); + if ($thisfill !== null) { + $this->writeFill($objWriter, $thisfill); + } } $objWriter->endElement(); // borders $objWriter->startElement('borders'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getBordersHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getBordersHashTable()->count()); // border for ($i = 0; $i < $this->getParentWriter()->getBordersHashTable()->count(); ++$i) { - $this->writeBorder($objWriter, $this->getParentWriter()->getBordersHashTable()->getByIndex($i)); + $thisborder = $this->getParentWriter()->getBordersHashTable()->getByIndex($i); + if ($thisborder !== null) { + $this->writeBorder($objWriter, $thisborder); + } } $objWriter->endElement(); // cellStyleXfs $objWriter->startElement('cellStyleXfs'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('numFmtId', 0); - $objWriter->writeAttribute('fontId', 0); - $objWriter->writeAttribute('fillId', 0); - $objWriter->writeAttribute('borderId', 0); + $objWriter->writeAttribute('numFmtId', '0'); + $objWriter->writeAttribute('fontId', '0'); + $objWriter->writeAttribute('fillId', '0'); + $objWriter->writeAttribute('borderId', '0'); $objWriter->endElement(); $objWriter->endElement(); // cellXfs $objWriter->startElement('cellXfs'); - $objWriter->writeAttribute('count', count($spreadsheet->getCellXfCollection())); + $objWriter->writeAttribute('count', (string) count($spreadsheet->getCellXfCollection())); // xf foreach ($spreadsheet->getCellXfCollection() as $cellXf) { @@ -109,24 +119,27 @@ class Style extends WriterPart // cellStyles $objWriter->startElement('cellStyles'); - $objWriter->writeAttribute('count', 1); + $objWriter->writeAttribute('count', '1'); // cellStyle $objWriter->startElement('cellStyle'); $objWriter->writeAttribute('name', 'Normal'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('builtinId', 0); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('builtinId', '0'); $objWriter->endElement(); $objWriter->endElement(); // dxfs $objWriter->startElement('dxfs'); - $objWriter->writeAttribute('count', $this->getParentWriter()->getStylesConditionalHashTable()->count()); + $objWriter->writeAttribute('count', (string) $this->getParentWriter()->getStylesConditionalHashTable()->count()); // dxf for ($i = 0; $i < $this->getParentWriter()->getStylesConditionalHashTable()->count(); ++$i) { - $this->writeCellStyleDxf($objWriter, $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i)->getStyle()); + $thisstyle = $this->getParentWriter()->getStylesConditionalHashTable()->getByIndex($i); + if ($thisstyle !== null) { + $this->writeCellStyleDxf($objWriter, $thisstyle->getStyle()); + } } $objWriter->endElement(); @@ -171,17 +184,19 @@ class Style extends WriterPart // gradientFill $objWriter->startElement('gradientFill'); - $objWriter->writeAttribute('type', $fill->getFillType()); - $objWriter->writeAttribute('degree', $fill->getRotation()); + $objWriter->writeAttribute('type', (string) $fill->getFillType()); + $objWriter->writeAttribute('degree', (string) $fill->getRotation()); // stop $objWriter->startElement('stop'); $objWriter->writeAttribute('position', '0'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getStartColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getStartColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -190,9 +205,11 @@ class Style extends WriterPart $objWriter->writeAttribute('position', '1'); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); - $objWriter->endElement(); + if ($fill->getEndColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $fill->getEndColor()->getARGB()); + $objWriter->endElement(); + } $objWriter->endElement(); @@ -220,7 +237,7 @@ class Style extends WriterPart // patternFill $objWriter->startElement('patternFill'); - $objWriter->writeAttribute('patternType', $fill->getFillType()); + $objWriter->writeAttribute('patternType', (string) $fill->getFillType()); if (self::writePatternColors($fill)) { // fgColor @@ -360,20 +377,20 @@ class Style extends WriterPart { // xf $objWriter->startElement('xf'); - $objWriter->writeAttribute('xfId', 0); - $objWriter->writeAttribute('fontId', (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); + $objWriter->writeAttribute('xfId', '0'); + $objWriter->writeAttribute('fontId', (string) (int) $this->getParentWriter()->getFontHashTable()->getIndexForHashCode($style->getFont()->getHashCode())); if ($style->getQuotePrefix()) { - $objWriter->writeAttribute('quotePrefix', 1); + $objWriter->writeAttribute('quotePrefix', '1'); } if ($style->getNumberFormat()->getBuiltInFormatCode() === false) { - $objWriter->writeAttribute('numFmtId', (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); + $objWriter->writeAttribute('numFmtId', (string) (int) ($this->getParentWriter()->getNumFmtHashTable()->getIndexForHashCode($style->getNumberFormat()->getHashCode()) + 164)); } else { - $objWriter->writeAttribute('numFmtId', (int) $style->getNumberFormat()->getBuiltInFormatCode()); + $objWriter->writeAttribute('numFmtId', (string) (int) $style->getNumberFormat()->getBuiltInFormatCode()); } - $objWriter->writeAttribute('fillId', (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); - $objWriter->writeAttribute('borderId', (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); + $objWriter->writeAttribute('fillId', (string) (int) $this->getParentWriter()->getFillHashTable()->getIndexForHashCode($style->getFill()->getHashCode())); + $objWriter->writeAttribute('borderId', (string) (int) $this->getParentWriter()->getBordersHashTable()->getIndexForHashCode($style->getBorders()->getHashCode())); // Apply styles? $objWriter->writeAttribute('applyFont', ($spreadsheet->getDefaultStyle()->getFont()->getHashCode() != $style->getFont()->getHashCode()) ? '1' : '0'); @@ -387,25 +404,31 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal !== '') { + $objWriter->writeAttribute('horizontal', $horizontal); + } + if ($vertical !== '') { + $objWriter->writeAttribute('vertical', $vertical); + } $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); $objWriter->writeAttribute('wrapText', ($style->getAlignment()->getWrapText() ? 'true' : 'false')); $objWriter->writeAttribute('shrinkToFit', ($style->getAlignment()->getShrinkToFit() ? 'true' : 'false')); if ($style->getAlignment()->getIndent() > 0) { - $objWriter->writeAttribute('indent', $style->getAlignment()->getIndent()); + $objWriter->writeAttribute('indent', (string) $style->getAlignment()->getIndent()); } if ($style->getAlignment()->getReadOrder() > 0) { - $objWriter->writeAttribute('readingOrder', $style->getAlignment()->getReadOrder()); + $objWriter->writeAttribute('readingOrder', (string) $style->getAlignment()->getReadOrder()); } $objWriter->endElement(); @@ -443,21 +466,23 @@ class Style extends WriterPart // alignment $objWriter->startElement('alignment'); - if ($style->getAlignment()->getHorizontal() !== null) { - $objWriter->writeAttribute('horizontal', $style->getAlignment()->getHorizontal()); + $horizontal = Alignment::HORIZONTAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getHorizontal()] ?? ''; + if ($horizontal) { + $objWriter->writeAttribute('horizontal', $horizontal); } - if ($style->getAlignment()->getVertical() !== null) { - $objWriter->writeAttribute('vertical', $style->getAlignment()->getVertical()); + $vertical = Alignment::VERTICAL_ALIGNMENT_FOR_XLSX[$style->getAlignment()->getVertical()] ?? ''; + if ($vertical) { + $objWriter->writeAttribute('vertical', $vertical); } if ($style->getAlignment()->getTextRotation() !== null) { $textRotation = 0; if ($style->getAlignment()->getTextRotation() >= 0) { $textRotation = $style->getAlignment()->getTextRotation(); - } elseif ($style->getAlignment()->getTextRotation() < 0) { + } else { $textRotation = 90 - $style->getAlignment()->getTextRotation(); } - $objWriter->writeAttribute('textRotation', $textRotation); + $objWriter->writeAttribute('textRotation', (string) $textRotation); } $objWriter->endElement(); @@ -465,7 +490,7 @@ class Style extends WriterPart $this->writeBorder($objWriter, $style->getBorders()); // protection - if (($style->getProtection()->getLocked() !== null) || ($style->getProtection()->getHidden() !== null)) { + if ((!empty($style->getProtection()->getLocked())) || (!empty($style->getProtection()->getHidden()))) { if ( $style->getProtection()->getLocked() !== Protection::PROTECTION_INHERIT || $style->getProtection()->getHidden() !== Protection::PROTECTION_INHERIT @@ -503,11 +528,13 @@ class Style extends WriterPart $objWriter->writeAttribute('style', $border->getBorderStyle()); // color - $objWriter->startElement('color'); - $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); - $objWriter->endElement(); + if ($border->getColor()->getARGB() !== null) { + $objWriter->startElement('color'); + $objWriter->writeAttribute('rgb', $border->getColor()->getARGB()); + $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); + } } } @@ -516,15 +543,15 @@ class Style extends WriterPart * * @param int $id Number Format identifier */ - private function writeNumFmt(XMLWriter $objWriter, NumberFormat $numberFormat, $id = 0): void + private function writeNumFmt(XMLWriter $objWriter, ?NumberFormat $numberFormat, $id = 0): void { // Translate formatcode - $formatCode = $numberFormat->getFormatCode(); + $formatCode = ($numberFormat === null) ? null : $numberFormat->getFormatCode(); // numFmt if ($formatCode !== null) { $objWriter->startElement('numFmt'); - $objWriter->writeAttribute('numFmtId', ($id + 164)); + $objWriter->writeAttribute('numFmtId', (string) ($id + 164)); $objWriter->writeAttribute('formatCode', $formatCode); $objWriter->endElement(); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Table.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Table.php new file mode 100644 index 00000000000..67dbd19bd84 --- /dev/null +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Table.php @@ -0,0 +1,111 @@ +getParentWriter()->getUseDiskCaching()) { + $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); + } else { + $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY); + } + + // XML header + $objWriter->startDocument('1.0', 'UTF-8', 'yes'); + + // Table + $name = 'Table' . $tableRef; + $range = $table->getRange(); + + $objWriter->startElement('table'); + $objWriter->writeAttribute('xml:space', 'preserve'); + $objWriter->writeAttribute('xmlns', 'http://schemas.openxmlformats.org/spreadsheetml/2006/main'); + $objWriter->writeAttribute('id', (string) $tableRef); + $objWriter->writeAttribute('name', $name); + $objWriter->writeAttribute('displayName', $table->getName() ?: $name); + $objWriter->writeAttribute('ref', $range); + $objWriter->writeAttribute('headerRowCount', $table->getShowHeaderRow() ? '1' : '0'); + $objWriter->writeAttribute('totalsRowCount', $table->getShowTotalsRow() ? '1' : '0'); + + // Table Boundaries + [$rangeStart, $rangeEnd] = Coordinate::rangeBoundaries($table->getRange()); + + // Table Auto Filter + if ($table->getShowHeaderRow()) { + $objWriter->startElement('autoFilter'); + $objWriter->writeAttribute('ref', $range); + foreach (range($rangeStart[0], $rangeEnd[0]) as $offset => $columnIndex) { + $column = $table->getColumnByOffset($offset); + + if (!$column->getShowFilterButton()) { + $objWriter->startElement('filterColumn'); + $objWriter->writeAttribute('colId', (string) $offset); + $objWriter->writeAttribute('hiddenButton', '1'); + $objWriter->endElement(); + } + } + $objWriter->endElement(); + } + + // Table Columns + $objWriter->startElement('tableColumns'); + $objWriter->writeAttribute('count', (string) ($rangeEnd[0] - $rangeStart[0] + 1)); + foreach (range($rangeStart[0], $rangeEnd[0]) as $offset => $columnIndex) { + $worksheet = $table->getWorksheet(); + if (!$worksheet) { + continue; + } + + $column = $table->getColumnByOffset($offset); + $cell = $worksheet->getCellByColumnAndRow($columnIndex, $rangeStart[1]); + + $objWriter->startElement('tableColumn'); + $objWriter->writeAttribute('id', (string) ($offset + 1)); + $objWriter->writeAttribute('name', $table->getShowHeaderRow() ? $cell->getValue() : 'Column' . ($offset + 1)); + + if ($table->getShowTotalsRow()) { + if ($column->getTotalsRowLabel()) { + $objWriter->writeAttribute('totalsRowLabel', $column->getTotalsRowLabel()); + } + if ($column->getTotalsRowFunction()) { + $objWriter->writeAttribute('totalsRowFunction', $column->getTotalsRowFunction()); + } + } + if ($column->getColumnFormula()) { + $objWriter->writeElement('calculatedColumnFormula', $column->getColumnFormula()); + } + + $objWriter->endElement(); + } + $objWriter->endElement(); + + // Table Styles + $objWriter->startElement('tableStyleInfo'); + $objWriter->writeAttribute('name', $table->getStyle()->getTheme()); + $objWriter->writeAttribute('showFirstColumn', $table->getStyle()->getShowFirstColumn() ? '1' : '0'); + $objWriter->writeAttribute('showLastColumn', $table->getStyle()->getShowLastColumn() ? '1' : '0'); + $objWriter->writeAttribute('showRowStripes', $table->getStyle()->getShowRowStripes() ? '1' : '0'); + $objWriter->writeAttribute('showColumnStripes', $table->getStyle()->getShowColumnStripes() ? '1' : '0'); + $objWriter->endElement(); + + $objWriter->endElement(); + + // Return + return $objWriter->getData(); + } +} diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php index f9d7197d7a6..7d08388daab 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Workbook.php @@ -104,14 +104,14 @@ class Workbook extends WriterPart // workbookView $objWriter->startElement('workbookView'); - $objWriter->writeAttribute('activeTab', $spreadsheet->getActiveSheetIndex()); + $objWriter->writeAttribute('activeTab', (string) $spreadsheet->getActiveSheetIndex()); $objWriter->writeAttribute('autoFilterDateGrouping', ($spreadsheet->getAutoFilterDateGrouping() ? 'true' : 'false')); - $objWriter->writeAttribute('firstSheet', $spreadsheet->getFirstSheetIndex()); + $objWriter->writeAttribute('firstSheet', (string) $spreadsheet->getFirstSheetIndex()); $objWriter->writeAttribute('minimized', ($spreadsheet->getMinimized() ? 'true' : 'false')); $objWriter->writeAttribute('showHorizontalScroll', ($spreadsheet->getShowHorizontalScroll() ? 'true' : 'false')); $objWriter->writeAttribute('showSheetTabs', ($spreadsheet->getShowSheetTabs() ? 'true' : 'false')); $objWriter->writeAttribute('showVerticalScroll', ($spreadsheet->getShowVerticalScroll() ? 'true' : 'false')); - $objWriter->writeAttribute('tabRatio', $spreadsheet->getTabRatio()); + $objWriter->writeAttribute('tabRatio', (string) $spreadsheet->getTabRatio()); $objWriter->writeAttribute('visibility', $spreadsheet->getVisibility()); $objWriter->endElement(); @@ -157,9 +157,9 @@ class Workbook extends WriterPart $objWriter->writeAttribute('calcId', '999999'); $objWriter->writeAttribute('calcMode', 'auto'); // fullCalcOnLoad isn't needed if we've recalculating for the save - $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? 1 : 0); - $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? 0 : 1); - $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? 0 : 1); + $objWriter->writeAttribute('calcCompleted', ($recalcRequired) ? '1' : '0'); + $objWriter->writeAttribute('fullCalcOnLoad', ($recalcRequired) ? '0' : '1'); + $objWriter->writeAttribute('forceFullCalc', ($recalcRequired) ? '0' : '1'); $objWriter->endElement(); } @@ -200,7 +200,7 @@ class Workbook extends WriterPart // Write sheet $objWriter->startElement('sheet'); $objWriter->writeAttribute('name', $worksheetName); - $objWriter->writeAttribute('sheetId', $worksheetId); + $objWriter->writeAttribute('sheetId', (string) $worksheetId); if ($sheetState !== 'visible' && $sheetState != '') { $objWriter->writeAttribute('state', $sheetState); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 494dc70ac6f..1aa4f1ca3c2 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; +use PhpOffice\PhpSpreadsheet\Calculation\Information\Value; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\RichText\RichText; @@ -26,7 +28,7 @@ class Worksheet extends WriterPart * * @return string XML Output */ - public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = null, $includeCharts = false) + public function writeWorksheet(PhpspreadsheetWorksheet $worksheet, $stringTable = [], $includeCharts = false) { // Create XML writer $objWriter = null; @@ -118,6 +120,9 @@ class Worksheet extends WriterPart // AlternateContent $this->writeAlternateContent($objWriter, $worksheet); + // Table + $this->writeTable($objWriter, $worksheet); + // ConditionalFormattingRuleExtensionList // (Must be inserted last. Not insert last, an Excel parse error will occur) $this->writeExtLst($objWriter, $worksheet); @@ -144,7 +149,7 @@ class Worksheet extends WriterPart } $autoFilterRange = $worksheet->getAutoFilter()->getRange(); if (!empty($autoFilterRange)) { - $objWriter->writeAttribute('filterMode', 1); + $objWriter->writeAttribute('filterMode', '1'); if (!$worksheet->getAutoFilter()->getEvaluated()) { $worksheet->getAutoFilter()->showHideRows(); } @@ -153,7 +158,7 @@ class Worksheet extends WriterPart // tabColor if ($worksheet->isTabColorSet()) { $objWriter->startElement('tabColor'); - $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB()); + $objWriter->writeAttribute('rgb', $worksheet->getTabColor()->getARGB() ?? ''); $objWriter->endElement(); } @@ -205,15 +210,15 @@ class Worksheet extends WriterPart // Zoom scales if ($worksheet->getSheetView()->getZoomScale() != 100) { - $objWriter->writeAttribute('zoomScale', $worksheet->getSheetView()->getZoomScale()); + $objWriter->writeAttribute('zoomScale', (string) $worksheet->getSheetView()->getZoomScale()); } if ($worksheet->getSheetView()->getZoomScaleNormal() != 100) { - $objWriter->writeAttribute('zoomScaleNormal', $worksheet->getSheetView()->getZoomScaleNormal()); + $objWriter->writeAttribute('zoomScaleNormal', (string) $worksheet->getSheetView()->getZoomScaleNormal()); } // Show zeros (Excel also writes this attribute only if set to false) if ($worksheet->getSheetView()->getShowZeros() === false) { - $objWriter->writeAttribute('showZeros', 0); + $objWriter->writeAttribute('showZeros', '0'); } // View Layout Type @@ -247,7 +252,7 @@ class Worksheet extends WriterPart // Pane $pane = ''; if ($worksheet->getFreezePane()) { - [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane() ?? ''); + [$xSplit, $ySplit] = Coordinate::coordinateFromString($worksheet->getFreezePane()); $xSplit = Coordinate::columnIndexFromString($xSplit); --$xSplit; --$ySplit; @@ -256,7 +261,7 @@ class Worksheet extends WriterPart $pane = 'topRight'; $objWriter->startElement('pane'); if ($xSplit > 0) { - $objWriter->writeAttribute('xSplit', $xSplit); + $objWriter->writeAttribute('xSplit', "$xSplit"); } if ($ySplit > 0) { $objWriter->writeAttribute('ySplit', $ySplit); @@ -313,10 +318,7 @@ class Worksheet extends WriterPart } // Set Zero Height row - if ( - (string) $worksheet->getDefaultRowDimension()->getZeroHeight() === '1' || - strtolower((string) $worksheet->getDefaultRowDimension()->getZeroHeight()) == 'true' - ) { + if ($worksheet->getDefaultRowDimension()->getZeroHeight()) { $objWriter->writeAttribute('zeroHeight', '1'); } @@ -332,7 +334,7 @@ class Worksheet extends WriterPart $outlineLevelRow = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelRow', (int) $outlineLevelRow); + $objWriter->writeAttribute('outlineLevelRow', (string) (int) $outlineLevelRow); // Outline level - column $outlineLevelCol = 0; @@ -341,7 +343,7 @@ class Worksheet extends WriterPart $outlineLevelCol = $dimension->getOutlineLevel(); } } - $objWriter->writeAttribute('outlineLevelCol', (int) $outlineLevelCol); + $objWriter->writeAttribute('outlineLevelCol', (string) (int) $outlineLevelCol); $objWriter->endElement(); } @@ -361,8 +363,8 @@ class Worksheet extends WriterPart foreach ($worksheet->getColumnDimensions() as $colDimension) { // col $objWriter->startElement('col'); - $objWriter->writeAttribute('min', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); - $objWriter->writeAttribute('max', Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('min', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); + $objWriter->writeAttribute('max', (string) Coordinate::columnIndexFromString($colDimension->getColumnIndex())); if ($colDimension->getWidth() < 0) { // No width set, apply default of 10 @@ -394,11 +396,11 @@ class Worksheet extends WriterPart // Outline level if ($colDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $colDimension->getOutlineLevel()); + $objWriter->writeAttribute('outlineLevel', (string) $colDimension->getOutlineLevel()); } // Style - $objWriter->writeAttribute('style', $colDimension->getXfIndex()); + $objWriter->writeAttribute('style', (string) $colDimension->getXfIndex()); $objWriter->endElement(); } @@ -421,7 +423,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute('algorithmName', $protection->getAlgorithm()); $objWriter->writeAttribute('hashValue', $protection->getPassword()); $objWriter->writeAttribute('saltValue', $protection->getSalt()); - $objWriter->writeAttribute('spinCount', $protection->getSpinCount()); + $objWriter->writeAttribute('spinCount', (string) $protection->getSpinCount()); } elseif ($protection->getPassword() !== '') { $objWriter->writeAttribute('password', $protection->getPassword()); } @@ -445,7 +447,7 @@ class Worksheet extends WriterPart $objWriter->endElement(); } - private static function writeAttributeIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeAttributeIf(XMLWriter $objWriter, ?bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeAttribute($attr, $val); @@ -459,7 +461,7 @@ class Worksheet extends WriterPart } } - private static function writeElementIf(XMLWriter $objWriter, $condition, string $attr, string $val): void + private static function writeElementIf(XMLWriter $objWriter, bool $condition, string $attr, string $val): void { if ($condition) { $objWriter->writeElement($attr, $val); @@ -468,37 +470,86 @@ class Worksheet extends WriterPart private static function writeOtherCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { + $conditions = $conditional->getConditions(); if ( $conditional->getConditionType() == Conditional::CONDITION_CELLIS - || $conditional->getConditionType() == Conditional::CONDITION_CONTAINSTEXT || $conditional->getConditionType() == Conditional::CONDITION_EXPRESSION + || !empty($conditions) ) { - foreach ($conditional->getConditions() as $formula) { + foreach ($conditions as $formula) { // Formula - $objWriter->writeElement('formula', Xlfn::addXlfn($formula)); + if (is_bool($formula)) { + $formula = $formula ? 'TRUE' : 'FALSE'; + } + $objWriter->writeElement('formula', Xlfn::addXlfn("$formula")); + } + } else { + if ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) { + // formula copied from ms xlsx xml source file + $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) { + // formula copied from ms xlsx xml source file + $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSERRORS) { + // formula copied from ms xlsx xml source file + $objWriter->writeElement('formula', 'ISERROR(' . $cellCoordinate . ')'); + } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSERRORS) { + // formula copied from ms xlsx xml source file + $objWriter->writeElement('formula', 'NOT(ISERROR(' . $cellCoordinate . '))'); + } + } + } + + private static function writeTimePeriodCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void + { + $txt = $conditional->getText(); + if (!empty($txt)) { + $objWriter->writeAttribute('timePeriod', $txt); + if (empty($conditional->getConditions())) { + if ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TODAY) { + $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_TOMORROW) { + $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()+1'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_YESTERDAY) { + $objWriter->writeElement('formula', 'FLOOR(' . $cellCoordinate . ')=TODAY()-1'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_7_DAYS) { + $objWriter->writeElement('formula', 'AND(TODAY()-FLOOR(' . $cellCoordinate . ',1)<=6,FLOOR(' . $cellCoordinate . ',1)<=TODAY())'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_WEEK) { + $objWriter->writeElement('formula', 'AND(TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)>=(WEEKDAY(TODAY())),TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)<(WEEKDAY(TODAY())+7))'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_THIS_WEEK) { + $objWriter->writeElement('formula', 'AND(TODAY()-ROUNDDOWN(' . $cellCoordinate . ',0)<=WEEKDAY(TODAY())-1,ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()<=7-WEEKDAY(TODAY()))'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_NEXT_WEEK) { + $objWriter->writeElement('formula', 'AND(ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()>(7-WEEKDAY(TODAY())),ROUNDDOWN(' . $cellCoordinate . ',0)-TODAY()<(15-WEEKDAY(TODAY())))'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_LAST_MONTH) { + $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(EDATE(TODAY(),0-1)),YEAR(' . $cellCoordinate . ')=YEAR(EDATE(TODAY(),0-1)))'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_THIS_MONTH) { + $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(TODAY()),YEAR(' . $cellCoordinate . ')=YEAR(TODAY()))'); + } elseif ($conditional->getOperatorType() == Conditional::TIMEPERIOD_NEXT_MONTH) { + $objWriter->writeElement('formula', 'AND(MONTH(' . $cellCoordinate . ')=MONTH(EDATE(TODAY(),0+1)),YEAR(' . $cellCoordinate . ')=YEAR(EDATE(TODAY(),0+1)))'); + } + } else { + $objWriter->writeElement('formula', (string) ($conditional->getConditions()[0])); } - } elseif ($conditional->getConditionType() == Conditional::CONDITION_CONTAINSBLANKS) { - // formula copied from ms xlsx xml source file - $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))=0'); - } elseif ($conditional->getConditionType() == Conditional::CONDITION_NOTCONTAINSBLANKS) { - // formula copied from ms xlsx xml source file - $objWriter->writeElement('formula', 'LEN(TRIM(' . $cellCoordinate . '))>0'); } } private static function writeTextCondElements(XMLWriter $objWriter, Conditional $conditional, string $cellCoordinate): void { $txt = $conditional->getText(); - if ($txt !== null) { + if (!empty($txt)) { $objWriter->writeAttribute('text', $txt); - if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) { - $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . ')))'); - } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH) { - $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"'); - } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH) { - $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',' . strlen($txt) . ')="' . $txt . '"'); - } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS) { - $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . '))'); + if (empty($conditional->getConditions())) { + if ($conditional->getOperatorType() == Conditional::OPERATOR_CONTAINSTEXT) { + $objWriter->writeElement('formula', 'NOT(ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . ')))'); + } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_BEGINSWITH) { + $objWriter->writeElement('formula', 'LEFT(' . $cellCoordinate . ',LEN("' . $txt . '"))="' . $txt . '"'); + } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_ENDSWITH) { + $objWriter->writeElement('formula', 'RIGHT(' . $cellCoordinate . ',LEN("' . $txt . '"))="' . $txt . '"'); + } elseif ($conditional->getOperatorType() == Conditional::OPERATOR_NOTCONTAINS) { + $objWriter->writeElement('formula', 'ISERROR(SEARCH("' . $txt . '",' . $cellCoordinate . '))'); + } + } else { + $objWriter->writeElement('formula', (string) ($conditional->getConditions()[0])); } } } @@ -517,7 +568,7 @@ class Worksheet extends WriterPart $objWriter->writeAttribute($attrKey, $val); } $minCfvo = $dataBar->getMinimumConditionalFormatValueObject(); - if ($minCfvo) { + if ($minCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $minCfvo->getType()); if ($minCfvo->getCellFormula()) { @@ -527,7 +578,7 @@ class Worksheet extends WriterPart } $maxCfvo = $dataBar->getMaximumConditionalFormatValueObject(); - if ($maxCfvo) { + if ($maxCfvo !== null) { $objWriter->startElementNs($prefix, 'cfvo', null); $objWriter->writeAttribute('type', $maxCfvo->getType()); if ($maxCfvo->getCellFormula()) { @@ -549,9 +600,8 @@ class Worksheet extends WriterPart $objWriter->endElement(); //end conditionalFormatting } - private static function writeDataBarElements(XMLWriter $objWriter, $dataBar): void + private static function writeDataBarElements(XMLWriter $objWriter, ?ConditionalDataBar $dataBar): void { - /** @var ConditionalDataBar $dataBar */ if ($dataBar) { $objWriter->startElement('dataBar'); self::writeAttributeIf($objWriter, null !== $dataBar->getShowValue(), 'showValue', $dataBar->getShowValue() ? '1' : '0'); @@ -601,57 +651,63 @@ class Worksheet extends WriterPart // Loop through styles in the current worksheet foreach ($worksheet->getConditionalStylesCollection() as $cellCoordinate => $conditionalStyles) { + $objWriter->startElement('conditionalFormatting'); + $objWriter->writeAttribute('sqref', $cellCoordinate); + foreach ($conditionalStyles as $conditional) { // WHY was this again? // if ($this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) == '') { // continue; // } - if ($conditional->getConditionType() != Conditional::CONDITION_NONE) { - // conditionalFormatting - $objWriter->startElement('conditionalFormatting'); - $objWriter->writeAttribute('sqref', $cellCoordinate); + // cfRule + $objWriter->startElement('cfRule'); + $objWriter->writeAttribute('type', $conditional->getConditionType()); + self::writeAttributeIf( + $objWriter, + ($conditional->getConditionType() != Conditional::CONDITION_DATABAR), + 'dxfId', + (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) + ); + $objWriter->writeAttribute('priority', (string) $id++); - // cfRule - $objWriter->startElement('cfRule'); - $objWriter->writeAttribute('type', $conditional->getConditionType()); - self::writeAttributeIf( - $objWriter, - ($conditional->getConditionType() != Conditional::CONDITION_DATABAR), - 'dxfId', - (string) $this->getParentWriter()->getStylesConditionalHashTable()->getIndexForHashCode($conditional->getHashCode()) - ); - $objWriter->writeAttribute('priority', $id++); - - self::writeAttributeif( - $objWriter, - ( - $conditional->getConditionType() === Conditional::CONDITION_CELLIS - || $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT - || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT - ) && $conditional->getOperatorType() !== Conditional::OPERATOR_NONE, - 'operator', - $conditional->getOperatorType() - ); - - self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), 'stopIfTrue', '1'); - - if ( - $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT + self::writeAttributeif( + $objWriter, + ( + $conditional->getConditionType() === Conditional::CONDITION_CELLIS + || $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT - ) { - self::writeTextCondElements($objWriter, $conditional, $cellCoordinate); - } else { - self::writeOtherCondElements($objWriter, $conditional, $cellCoordinate); - } + || $conditional->getConditionType() === Conditional::CONDITION_BEGINSWITH + || $conditional->getConditionType() === Conditional::CONDITION_ENDSWITH + ) && $conditional->getOperatorType() !== Conditional::OPERATOR_NONE, + 'operator', + $conditional->getOperatorType() + ); - // - self::writeDataBarElements($objWriter, $conditional->getDataBar()); + self::writeAttributeIf($objWriter, $conditional->getStopIfTrue(), 'stopIfTrue', '1'); - $objWriter->endElement(); //end cfRule + $cellRange = Coordinate::splitRange(str_replace('$', '', strtoupper($cellCoordinate))); + [$topLeftCell] = $cellRange[0]; - $objWriter->endElement(); + if ( + $conditional->getConditionType() === Conditional::CONDITION_CONTAINSTEXT + || $conditional->getConditionType() === Conditional::CONDITION_NOTCONTAINSTEXT + || $conditional->getConditionType() === Conditional::CONDITION_BEGINSWITH + || $conditional->getConditionType() === Conditional::CONDITION_ENDSWITH + ) { + self::writeTextCondElements($objWriter, $conditional, $topLeftCell); + } elseif ($conditional->getConditionType() === Conditional::CONDITION_TIMEPERIOD) { + self::writeTimePeriodCondElements($objWriter, $conditional, $topLeftCell); + } else { + self::writeOtherCondElements($objWriter, $conditional, $topLeftCell); } + + // + self::writeDataBarElements($objWriter, $conditional->getDataBar()); + + $objWriter->endElement(); //end cfRule } + + $objWriter->endElement(); //end conditionalFormatting } } @@ -667,7 +723,7 @@ class Worksheet extends WriterPart if (!empty($dataValidationCollection)) { $dataValidationCollection = Coordinate::mergeRangesInCollection($dataValidationCollection); $objWriter->startElement('dataValidations'); - $objWriter->writeAttribute('count', count($dataValidationCollection)); + $objWriter->writeAttribute('count', (string) count($dataValidationCollection)); foreach ($dataValidationCollection as $coordinate => $dv) { $objWriter->startElement('dataValidation'); @@ -865,11 +921,11 @@ class Worksheet extends WriterPart $rules = $column->getRules(); if (count($rules) > 0) { $objWriter->startElement('filterColumn'); - $objWriter->writeAttribute('colId', $worksheet->getAutoFilter()->getColumnOffset($columnID)); + $objWriter->writeAttribute('colId', (string) $worksheet->getAutoFilter()->getColumnOffset($columnID)); $objWriter->startElement($column->getFilterType()); if ($column->getJoin() == Column::AUTOFILTER_COLUMN_JOIN_AND) { - $objWriter->writeAttribute('and', 1); + $objWriter->writeAttribute('and', '1'); } foreach ($rules as $rule) { @@ -879,7 +935,7 @@ class Worksheet extends WriterPart ($rule->getValue() === '') ) { // Filter rule for Blanks - $objWriter->writeAttribute('blank', 1); + $objWriter->writeAttribute('blank', '1'); } elseif ($rule->getRuleType() === Rule::AUTOFILTER_RULETYPE_DYNAMICFILTER) { // Dynamic Filter Rule $objWriter->writeAttribute('type', $rule->getGrouping()); @@ -936,6 +992,25 @@ class Worksheet extends WriterPart } } + /** + * Write Table. + */ + private function writeTable(XMLWriter $objWriter, PhpspreadsheetWorksheet $worksheet): void + { + $tableCount = $worksheet->getTableCollection()->count(); + + $objWriter->startElement('tableParts'); + $objWriter->writeAttribute('count', (string) $tableCount); + + for ($t = 1; $t <= $tableCount; ++$t) { + $objWriter->startElement('tablePart'); + $objWriter->writeAttribute('r:id', 'rId_table_' . $t); + $objWriter->endElement(); + } + + $objWriter->endElement(); + } + /** * Write PageSetup. */ @@ -943,24 +1018,24 @@ class Worksheet extends WriterPart { // pageSetup $objWriter->startElement('pageSetup'); - $objWriter->writeAttribute('paperSize', $worksheet->getPageSetup()->getPaperSize()); + $objWriter->writeAttribute('paperSize', (string) $worksheet->getPageSetup()->getPaperSize()); $objWriter->writeAttribute('orientation', $worksheet->getPageSetup()->getOrientation()); if ($worksheet->getPageSetup()->getScale() !== null) { - $objWriter->writeAttribute('scale', $worksheet->getPageSetup()->getScale()); + $objWriter->writeAttribute('scale', (string) $worksheet->getPageSetup()->getScale()); } if ($worksheet->getPageSetup()->getFitToHeight() !== null) { - $objWriter->writeAttribute('fitToHeight', $worksheet->getPageSetup()->getFitToHeight()); + $objWriter->writeAttribute('fitToHeight', (string) $worksheet->getPageSetup()->getFitToHeight()); } else { $objWriter->writeAttribute('fitToHeight', '0'); } if ($worksheet->getPageSetup()->getFitToWidth() !== null) { - $objWriter->writeAttribute('fitToWidth', $worksheet->getPageSetup()->getFitToWidth()); + $objWriter->writeAttribute('fitToWidth', (string) $worksheet->getPageSetup()->getFitToWidth()); } else { $objWriter->writeAttribute('fitToWidth', '0'); } - if ($worksheet->getPageSetup()->getFirstPageNumber() !== null) { - $objWriter->writeAttribute('firstPageNumber', $worksheet->getPageSetup()->getFirstPageNumber()); + if (!empty($worksheet->getPageSetup()->getFirstPageNumber())) { + $objWriter->writeAttribute('firstPageNumber', (string) $worksheet->getPageSetup()->getFirstPageNumber()); $objWriter->writeAttribute('useFirstPageNumber', '1'); } $objWriter->writeAttribute('pageOrder', $worksheet->getPageSetup()->getPageOrder()); @@ -1013,8 +1088,8 @@ class Worksheet extends WriterPart // rowBreaks if (!empty($aRowBreaks)) { $objWriter->startElement('rowBreaks'); - $objWriter->writeAttribute('count', count($aRowBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aRowBreaks)); + $objWriter->writeAttribute('count', (string) count($aRowBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aRowBreaks)); foreach ($aRowBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); @@ -1031,14 +1106,14 @@ class Worksheet extends WriterPart // Second, write column breaks if (!empty($aColumnBreaks)) { $objWriter->startElement('colBreaks'); - $objWriter->writeAttribute('count', count($aColumnBreaks)); - $objWriter->writeAttribute('manualBreakCount', count($aColumnBreaks)); + $objWriter->writeAttribute('count', (string) count($aColumnBreaks)); + $objWriter->writeAttribute('manualBreakCount', (string) count($aColumnBreaks)); foreach ($aColumnBreaks as $cell) { $coords = Coordinate::coordinateFromString($cell); $objWriter->startElement('brk'); - $objWriter->writeAttribute('id', Coordinate::columnIndexFromString($coords[0]) - 1); + $objWriter->writeAttribute('id', (string) (Coordinate::columnIndexFromString($coords[0]) - 1)); $objWriter->writeAttribute('man', '1'); $objWriter->endElement(); } @@ -1066,64 +1141,74 @@ class Worksheet extends WriterPart // Highest row number $highestRow = $worksheet->getHighestRow(); - // Loop through cells + // Loop through cells building a comma-separated list of the columns in each row + // This is a trade-off between the memory usage that is required for a full array of columns, + // and execution speed + /** @var array $cellsByRow */ $cellsByRow = []; foreach ($worksheet->getCoordinates() as $coordinate) { - $cellAddress = Coordinate::coordinateFromString($coordinate); - $cellsByRow[$cellAddress[1]][] = $coordinate; + [$column, $row] = Coordinate::coordinateFromString($coordinate); + $cellsByRow[$row] = $cellsByRow[$row] ?? ''; + $cellsByRow[$row] .= "{$column},"; } $currentRow = 0; while ($currentRow++ < $highestRow) { - // Get row dimension - $rowDimension = $worksheet->getRowDimension($currentRow); + $isRowSet = isset($cellsByRow[$currentRow]); + if ($isRowSet || $worksheet->rowDimensionExists($currentRow)) { + // Get row dimension + $rowDimension = $worksheet->getRowDimension($currentRow); - // Write current row? - $writeCurrentRow = isset($cellsByRow[$currentRow]) || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() == false || $rowDimension->getCollapsed() == true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null; + // Write current row? + $writeCurrentRow = $isRowSet || $rowDimension->getRowHeight() >= 0 || $rowDimension->getVisible() === false || $rowDimension->getCollapsed() === true || $rowDimension->getOutlineLevel() > 0 || $rowDimension->getXfIndex() !== null; - if ($writeCurrentRow) { - // Start a new row - $objWriter->startElement('row'); - $objWriter->writeAttribute('r', $currentRow); - $objWriter->writeAttribute('spans', '1:' . $colCount); + if ($writeCurrentRow) { + // Start a new row + $objWriter->startElement('row'); + $objWriter->writeAttribute('r', "$currentRow"); + $objWriter->writeAttribute('spans', '1:' . $colCount); - // Row dimensions - if ($rowDimension->getRowHeight() >= 0) { - $objWriter->writeAttribute('customHeight', '1'); - $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight())); - } - - // Row visibility - if (!$rowDimension->getVisible() === true) { - $objWriter->writeAttribute('hidden', 'true'); - } - - // Collapsed - if ($rowDimension->getCollapsed() === true) { - $objWriter->writeAttribute('collapsed', 'true'); - } - - // Outline level - if ($rowDimension->getOutlineLevel() > 0) { - $objWriter->writeAttribute('outlineLevel', $rowDimension->getOutlineLevel()); - } - - // Style - if ($rowDimension->getXfIndex() !== null) { - $objWriter->writeAttribute('s', $rowDimension->getXfIndex()); - $objWriter->writeAttribute('customFormat', '1'); - } - - // Write cells - if (isset($cellsByRow[$currentRow])) { - foreach ($cellsByRow[$currentRow] as $cellAddress) { - // Write cell - $this->writeCell($objWriter, $worksheet, $cellAddress, $aFlippedStringTable); + // Row dimensions + if ($rowDimension->getRowHeight() >= 0) { + $objWriter->writeAttribute('customHeight', '1'); + $objWriter->writeAttribute('ht', StringHelper::formatNumber($rowDimension->getRowHeight())); } - } - // End row - $objWriter->endElement(); + // Row visibility + if (!$rowDimension->getVisible() === true) { + $objWriter->writeAttribute('hidden', 'true'); + } + + // Collapsed + if ($rowDimension->getCollapsed() === true) { + $objWriter->writeAttribute('collapsed', 'true'); + } + + // Outline level + if ($rowDimension->getOutlineLevel() > 0) { + $objWriter->writeAttribute('outlineLevel', (string) $rowDimension->getOutlineLevel()); + } + + // Style + if ($rowDimension->getXfIndex() !== null) { + $objWriter->writeAttribute('s', (string) $rowDimension->getXfIndex()); + $objWriter->writeAttribute('customFormat', '1'); + } + + // Write cells + if (isset($cellsByRow[$currentRow])) { + // We have a comma-separated list of column names (with a trailing entry); split to an array + $columnsInRow = explode(',', $cellsByRow[$currentRow]); + array_pop($columnsInRow); + foreach ($columnsInRow as $column) { + // Write cell + $this->writeCell($objWriter, $worksheet, "{$column}{$currentRow}", $aFlippedStringTable); + } + } + + // End row + $objWriter->endElement(); + } } } @@ -1137,11 +1222,13 @@ class Worksheet extends WriterPart { $objWriter->writeAttribute('t', $mappedType); if (!$cellValue instanceof RichText) { + $objWriter->startElement('is'); $objWriter->writeElement( 't', StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue, Settings::htmlEntityFlags())) ); - } elseif ($cellValue instanceof RichText) { + $objWriter->endElement(); + } else { $objWriter->startElement('is'); $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue); $objWriter->endElement(); @@ -1175,7 +1262,7 @@ class Worksheet extends WriterPart $cellValue = $cellValue . '.0'; } } - $objWriter->writeElement('v', $cellValue); + $objWriter->writeElement('v', "$cellValue"); } private function writeCellBoolean(XMLWriter $objWriter, string $mappedType, bool $cellValue): void @@ -1196,7 +1283,7 @@ class Worksheet extends WriterPart { $calculatedValue = $this->getParentWriter()->getPreCalculateFormulas() ? $cell->getCalculatedValue() : $cellValue; if (is_string($calculatedValue)) { - if (\PhpOffice\PhpSpreadsheet\Calculation\Functions::isError($calculatedValue)) { + if (ErrorValue::isError($calculatedValue)) { $this->writeCellError($objWriter, 'e', $cellValue, $calculatedValue); return; @@ -1223,7 +1310,7 @@ class Worksheet extends WriterPart $objWriter, $this->getParentWriter()->getOffice2003Compatibility() === false, 'v', - ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue, 0, 1) !== '#') + ($this->getParentWriter()->getPreCalculateFormulas() && !is_array($calculatedValue) && substr($calculatedValue ?? '', 0, 1) !== '#') ? StringHelper::formatNumber($calculatedValue) : '0' ); } @@ -1244,7 +1331,7 @@ class Worksheet extends WriterPart // Sheet styles $xfi = $pCell->getXfIndex(); - self::writeAttributeIf($objWriter, $xfi, 's', $xfi); + self::writeAttributeIf($objWriter, (bool) $xfi, 's', "$xfi"); // If cell value is supplied, write cell value $cellValue = $pCell->getValue(); @@ -1361,7 +1448,6 @@ class Worksheet extends WriterPart /** @var Conditional $conditional */ foreach ($conditionalStyles as $conditional) { $dataBar = $conditional->getDataBar(); - // @phpstan-ignore-next-line if ($dataBar && $dataBar->getConditionalFormattingRuleExt()) { $conditionalFormattingRuleExtList[] = $dataBar->getConditionalFormattingRuleExt(); } diff --git a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php index c88ef245b63..a1bdf96a5d6 100644 --- a/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php +++ b/lib/phpspreadsheet/vendor/phpoffice/phpspreadsheet/src/PhpSpreadsheet/Writer/Xlsx/Xlfn.php @@ -144,6 +144,9 @@ class Xlfn . '|call' . '|let' . '|register[.]id' + . '|textafter' + . '|textbefore' + . '|textsplit' . '|valuetotext' . ')(?=\\s*[(])/i'; @@ -152,7 +155,7 @@ class Xlfn */ public static function addXlfn(string $funcstring): string { - return preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring); + return (string) preg_replace(self::XLFNREGEXP, '_xlfn.$1', $funcstring); } /**