From 30bfd9e278ecdbfed7b0677e3afec2a4c1b44036 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Mon, 7 Feb 2022 22:27:46 +0100 Subject: [PATCH] Ensure full support for vector/matrix combinations and for asymetric matrix/matrix arguments Re-baseline phpstan --- phpstan-baseline.neon | 91 +++++++++-------- .../Calculation/ArrayEnabled.php | 98 ++++++++++++------- .../Engine/ArrayArgumentHelper.php | 54 ++++++++-- .../Functions/MathTrig/AbsTest.php | 2 +- .../Functions/MathTrig/RoundTest.php | 67 +++++++++++++ 5 files changed, 225 insertions(+), 87 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7d9bd63ac2..25f0fc47ca 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -66,8 +66,13 @@ parameters: path: src/PhpSpreadsheet/Calculation/Calculation.php - - message: "#^Cannot access offset int(<0, max>)? on mixed\\.$#" - count: 16 + message: "#^Cannot access offset int on mixed\\.$#" + count: 10 + path: src/PhpSpreadsheet/Calculation/Calculation.php + + - + message: "#^Cannot access offset int\\<0, max\\> on mixed\\.$#" + count: 6 path: src/PhpSpreadsheet/Calculation/Calculation.php - @@ -1461,7 +1466,7 @@ parameters: path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php - - message: "#^Parameter \\#1 \\$array_arg of function uasort expects array()?, mixed given\\.$#" + message: "#^Parameter \\#1 \\$array_arg of function uasort expects array\\, mixed given\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -1471,54 +1476,34 @@ parameters: path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php - - message: "#^Parameter \\#2 \\$callback of function uasort expects callable\\((mixed|T), (mixed|T)\\)\\: int, array\\{'self', 'vlookupSort'\\} given\\.$#" + message: "#^Parameter \\#2 \\$callback of function uasort expects callable\\(T, T\\)\\: int, array\\{'self', 'vlookupSort'\\} given\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php - - message: "#^Parameter \\#1 \\$number of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Trig\\\\Cosine\\:\\:acos\\(\\) expects float, mixed given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig.php - - - - message: "#^Parameter \\#1 \\$number of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Trig\\\\Cosine\\:\\:acosh\\(\\) expects float, mixed given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig.php - - - - message: "#^Parameter \\#1 \\$number of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Trig\\\\Sine\\:\\:asin\\(\\) expects float, mixed given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig.php - - - - message: "#^Parameter \\#1 \\$number of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Trig\\\\Sine\\:\\:asinh\\(\\) expects float, mixed given\\.$#" + message: "#^Cannot cast mixed to string\\.$#" count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig.php + path: src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php - - message: "#^Parameter \\#1 \\$number of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Trig\\\\Tangent\\:\\:atan\\(\\) expects float, mixed given\\.$#" - count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig.php + message: "#^Binary operation \"/\" between array\\|float\\|int\\|string and array\\|float\\|int\\|string results in an error\\.$#" + count: 2 + path: src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php - - message: "#^Parameter \\#1 \\$number of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Trig\\\\Tangent\\:\\:atanh\\(\\) expects float, mixed given\\.$#" + message: "#^Parameter \\#1 \\$factVal of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Factorial\\:\\:fact\\(\\) expects array\\|float, int\\\\|int\\<0, max\\> given\\.$#" count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig.php + path: src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php - - message: "#^Cannot cast mixed to string\\.$#" + message: "#^Binary operation \"/\" between array\\|float\\|int\\|string and float\\|int results in an error\\.$#" count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php - - - - message: "#^Binary operation \"/\" between float\\|int\\|string and float\\|int\\|string results in an error\\.$#" - count: 2 - path: src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php + path: src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php - - message: "#^Binary operation \"/\" between float\\|int\\|string and float\\|int results in an error\\.$#" + message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\IntClass\\:\\:evaluate\\(\\) should return array\\|string but returns int\\.$#" count: 1 - path: src/PhpSpreadsheet/Calculation/MathTrig/Factorial.php + path: src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php - message: "#^Cannot call method getWorksheet\\(\\) on mixed\\.$#" @@ -1810,6 +1795,11 @@ parameters: count: 1 path: src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php + - + message: "#^Parameter \\#1 \\$factVal of static method PhpOffice\\\\PhpSpreadsheet\\\\Calculation\\\\MathTrig\\\\Factorial\\:\\:fact\\(\\) expects array\\|float, int\\<0, max\\> given\\.$#" + count: 1 + path: src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php + - message: "#^Binary operation \"\\-\" between float\\|string and float\\|int\\|numeric\\-string results in an error\\.$#" count: 1 @@ -1841,7 +1831,7 @@ parameters: path: src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php - - message: "#^Binary operation \"/\" between float\\|int\\|string and float\\|int\\|string results in an error\\.$#" + message: "#^Binary operation \"/\" between array\\|float\\|int\\|string and array\\|float\\|int\\|string results in an error\\.$#" count: 1 path: src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -6046,8 +6036,13 @@ parameters: path: src/PhpSpreadsheet/Worksheet/Worksheet.php - - message: "#^Parameter \\#1 \\$row of method PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Cells\\:\\:removeRow\\(\\) expects string, int(<1, max>)? given\\.$#" - count: 2 + message: "#^Parameter \\#1 \\$row of method PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Cells\\:\\:removeRow\\(\\) expects string, int given\\.$#" + count: 1 + path: src/PhpSpreadsheet/Worksheet/Worksheet.php + + - + message: "#^Parameter \\#1 \\$row of method PhpOffice\\\\PhpSpreadsheet\\\\Collection\\\\Cells\\:\\:removeRow\\(\\) expects string, int\\<1, max\\> given\\.$#" + count: 1 path: src/PhpSpreadsheet/Worksheet/Worksheet.php - @@ -6996,7 +6991,7 @@ parameters: path: src/PhpSpreadsheet/Writer/Xlsx.php - - message: "#^Cannot access offset int(<0, max>)? on mixed\\.$#" + message: "#^Cannot access offset int\\<0, max\\> on mixed\\.$#" count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -7161,7 +7156,7 @@ parameters: path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php - - message: "#^Parameter \\#1 \\$index of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getChartByIndex\\(\\) expects string, int(<0, max>)? given\\.$#" + message: "#^Parameter \\#1 \\$index of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getChartByIndex\\(\\) expects string, int\\<0, max\\> given\\.$#" count: 1 path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -7401,7 +7396,7 @@ parameters: path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - message: "#^Offset int(<1, max>)? on array\\\\> in isset\\(\\) does not exist\\.$#" + message: "#^Offset int\\<1, max\\> on array\\\\> in isset\\(\\) does not exist\\.$#" count: 2 path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -7436,8 +7431,18 @@ parameters: path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - - message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, (int|int\\<(0|1), max\\>) given\\.$#" - count: 27 + message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#" + count: 15 + path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php + + - + message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#" + count: 3 + path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php + + - + message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<1, max\\> given\\.$#" + count: 9 path: src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php - diff --git a/src/PhpSpreadsheet/Calculation/ArrayEnabled.php b/src/PhpSpreadsheet/Calculation/ArrayEnabled.php index 66d1b84698..b1db63d217 100644 --- a/src/PhpSpreadsheet/Calculation/ArrayEnabled.php +++ b/src/PhpSpreadsheet/Calculation/ArrayEnabled.php @@ -54,63 +54,91 @@ protected static function evaluateArrayArguments(callable $method, ...$arguments return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments); } - $rowVectorIndexes = self::$arrayArgumentHelper->getRowVectors(); - $columnVectorIndexes = self::$arrayArgumentHelper->getColumnVectors(); - - // Logic for a two row vectors or two column vectors - if (count($rowVectorIndexes) === 2 && count($columnVectorIndexes) === 0) { - return self::evaluateRowVectorPair($method, $rowVectorIndexes, ...$arguments); - } elseif (count($rowVectorIndexes) === 0 && count($columnVectorIndexes) === 2) { - return self::evaluateColumnVectorPair($method, $columnVectorIndexes, ...$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); } - // If we have multiple arrays, and they don't match a row vector/column vector pattern, - // or two row vectors and two column vectors, - // then we drop through to an error return for the moment - // Still need to work out the logic for multiple matrices as array arguments, - // or when we have more than two arrays + // Still need to work out the logic for more than two array arguments, return ['#VALUE!']; } /** * @param mixed ...$arguments */ - private static function evaluateRowVectorPair(callable $method, array $vectorIndexes, ...$arguments): array + private static function evaluateVectorMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array { - $vector2 = array_pop($vectorIndexes); - $vectorValues2 = Functions::flattenArray($arguments[$vector2]); - $vector1 = array_pop($vectorIndexes); - $vectorValues1 = Functions::flattenArray($arguments[$vector1]); + $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 = []; - foreach ($vectorValues1 as $index => $value1) { - $value2 = $vectorValues2[$index]; - $arguments[$vector1] = $value1; - $arguments[$vector2] = $value2; - - $result[] = $method(...$arguments); + 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]; + return $result; } /** * @param mixed ...$arguments */ - private static function evaluateColumnVectorPair(callable $method, array $vectorIndexes, ...$arguments): array + private static function evaluateMatrixPair(callable $method, array $matrixIndexes, ...$arguments): array { - $vector2 = array_pop($vectorIndexes); - $vectorValues2 = Functions::flattenArray($arguments[$vector2]); - $vector1 = array_pop($vectorIndexes); - $vectorValues1 = Functions::flattenArray($arguments[$vector1]); + $matrix2 = array_pop($matrixIndexes); + /** @var array $matrixValues2 */ + $matrixValues2 = $arguments[$matrix2]; + $matrix1 = array_pop($matrixIndexes); + /** @var array $matrixValues1 */ + $matrixValues1 = $arguments[$matrix1]; $result = []; - foreach ($vectorValues1 as $index => $value1) { - $value2 = $vectorValues2[$index]; - $arguments[$vector1] = $value1; - $arguments[$vector2] = $value2; + foreach ($matrixValues1 as $rowIndex => $row) { + foreach ($row as $columnIndex => $value1) { + if (isset($matrixValues2[$rowIndex][$columnIndex]) === false) { + continue; + } - $result[] = [$method(...$arguments)]; + $value2 = $matrixValues2[$rowIndex][$columnIndex]; + $arguments[$matrix1] = $value1; + $arguments[$matrix2] = $value2; + + $result[$rowIndex][$columnIndex] = $method(...$arguments); + } } return $result; diff --git a/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php b/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php index c32be3eacb..f28793345d 100644 --- a/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php +++ b/src/PhpSpreadsheet/Calculation/Engine/ArrayArgumentHelper.php @@ -75,7 +75,14 @@ public function getFirstArrayArgumentNumber(): int return 0; } - public function getRowVectors(): array + public function getSingleRowVector(): ?int + { + $rowVectors = $this->getRowVectors(); + + return count($rowVectors) === 1 ? array_pop($rowVectors) : null; + } + + private function getRowVectors(): array { $rowVectors = []; for ($index = 0; $index < $this->argumentCount; ++$index) { @@ -87,14 +94,14 @@ public function getRowVectors(): array return $rowVectors; } - public function getSingleRowVector(): ?int + public function getSingleColumnVector(): ?int { - $rowVectors = $this->getRowVectors(); + $columnVectors = $this->getColumnVectors(); - return count($rowVectors) === 1 ? array_pop($rowVectors) : null; + return count($columnVectors) === 1 ? array_pop($columnVectors) : null; } - public function getColumnVectors(): array + private function getColumnVectors(): array { $columnVectors = []; for ($index = 0; $index < $this->argumentCount; ++$index) { @@ -106,11 +113,42 @@ public function getColumnVectors(): array return $columnVectors; } - public function getSingleColumnVector(): ?int + public function getMatrixPair(): array { - $columnVectors = $this->getColumnVectors(); + for ($i = 0; $i < ($this->argumentCount - 1); ++$i) { + for ($j = $i + 1; $j < $this->argumentCount; ++$j) { + if (isset($this->rows[$i], $this->rows[$j])) { + return [$i, $j]; + } + } + } - return count($columnVectors) === 1 ? array_pop($columnVectors) : null; + 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 diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php index eac5fd4049..fda464eddb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php @@ -48,7 +48,7 @@ public function providerAbsArray(): array return [ 'row vector' => [[[1, 0, 1]], '{-1, 0, 1}'], 'column vector' => [[[1], [0], [1]], '{-1; 0; 1}'], - 'matrix' => [[[1, 0], [1, 1]], '{-1, 0; 1, -1}'], + 'matrix' => [[[1, 0], [1, 1.4]], '{-1, 0; 1, -1.4}'], ]; } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php index 3cfb33a1e9..87e683ca9b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php @@ -95,6 +95,73 @@ public function providerRoundArray(): array '{0.14527; 1.37250; -931.68290; 3.14159265}', '{1; 2; 3; 4}', ], + 'Two matching square matrices' => [ + [ + [10000, 46000, 78900], + [23460, 56789, 89012.3], + [34567.89, 67890.123, 90123.4568], + ], + '{12345.67890, 45678.90123, 78901.23456; 23456.78901, 56789.01234, 89012.34567; 34567.89012, 67890.12345, 90123.45678}', + '{-4, -3, -2; -1, 0, 1; 2, 3, 4}', + ], + 'Two paired 2x3 matrices' => [ + [ + [10000, 46000, 78900], + [23460, 56789, 89012.3], + ], + '{12345.67890, 45678.90123, 78901.23456; 23456.78901, 56789.01234, 89012.34567}', + '{-4, -3, -2; -1, 0, 1}', + ], + 'Two paired 3x2 matrices' => [ + [ + [10000, 46000], + [23460, 56789], + [34567.89, 67890.123], + ], + '{12345.67890, 45678.90123; 23456.78901, 56789.01234; 34567.89012, 67890.12345}', + '{-4, -3; -1, 0; 2, 3}', + ], + 'Two mismatched matrices (2x3 and 2x2)' => [ + [ + [10000, 46000], + [23460, 56789], + ], + '{12345.67890, 45678.90123, 78901.23456; 23456.78901, 56789.01234, 89012.34567}', + '{-4, -3; -1, 0}', + ], + 'Two mismatched matrices (3x2 and 2x1 vector)' => [ + [ + [10000, 50000], + [23460, 56790], + ], + '{12345.67890, 45678.90123; 23456.78901, 56789.01234; 34567.89012, 67890.12345}', + '{-4; -1}', + ], + 'Two mismatched matrices (3x1 vector and 2x2)' => [ + [ + [10000, 12000], + [23460, 23457], + ], + '{12345.67890; 23456.78901; 34567.89012}', + '{-4, -3; -1, 0}', + ], + 'Two mismatched matrices (1x3 vector and 2x2)' => [ + [ + [10000, 46000], + [12350, 45679], + ], + '{12345.67890, 45678.90123, 78901.23456}', + '{-4, -3; -1, 0}', + ], + 'Larger mismatched matrices (5x1 vector and 3x3)' => [ + [ + [3.1, 6.28, 9.425], + [12.6, 15.71, 18.85], + [22, 25.13, 28.274], + ], + '{3.14159265, 6.2831853, 9.424777959; 12.5663706, 15.70796325, 18.8495559; 21.99114855, 25.1327412, 28.27433385}', + '{1, 2, 3, 4, 5}', + ], ]; } }