diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 1e34628462..abb4209e9f 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1120,91 +1120,6 @@ parameters: count: 1 path: src/PhpSpreadsheet/Shared/Escher/DggContainer/BstoreContainer/BSE.php - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:__construct\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:arrayLeftDivide\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:arrayLeftDivideEquals\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:arrayRightDivide\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:arrayRightDivideEquals\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:arrayTimes\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:arrayTimesEquals\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:concat\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:getMatrix\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:minus\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:minusEquals\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:plus\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:plusEquals\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:power\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:times\\(\\) has parameter \\$args with no type specified\\.$#" - count: 1 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Parameter \\#3 \\$c of method PhpOffice\\\\PhpSpreadsheet\\\\Shared\\\\JAMA\\\\Matrix\\:\\:set\\(\\) expects float\\|int\\|null, string given\\.$#" - count: 2 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - - - message: "#^Unreachable statement \\- code above always terminates\\.$#" - count: 14 - path: src/PhpSpreadsheet/Shared/JAMA/Matrix.php - - message: "#^Cannot access offset 1 on array\\|false\\.$#" count: 1 diff --git a/phpstan-conditional.php b/phpstan-conditional.php index eeb21d1daa..1d0c15a9dd 100644 --- a/phpstan-conditional.php +++ b/phpstan-conditional.php @@ -74,11 +74,6 @@ 'path' => __DIR__ . '/src/PhpSpreadsheet/Calculation/TextData/Text.php', 'count' => 1, ]; - $config['parameters']['ignoreErrors'][] = [ - 'message' => '#^Property PhpOffice\\PhpSpreadsheet\\Shared\\JAMA\\Matrix::$A (array) does not accept array|false>|false.$#', - 'path' => __DIR__ . '/src/PhpSpreadsheet/Shared/JAMA/Matrix.php', - 'count' => 2, - ]; } else { // Flagged in Php8+ - unsure how to correct code $config['parameters']['ignoreErrors'][] = [ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1c4b531f37..9b195eb8f8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -4,10 +4,6 @@ ./src - - ./src/PhpSpreadsheet/Shared/JAMA - ./src/PhpSpreadsheet/Writer/PDF - diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 161c1572ee..0ca114a0b1 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -6,8 +6,6 @@ use PhpOffice\PhpSpreadsheet\Calculation\Engine\CyclicReferenceStack; use PhpOffice\PhpSpreadsheet\Calculation\Engine\Logger; use PhpOffice\PhpSpreadsheet\Calculation\Engine\Operands; -use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; -use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError; use PhpOffice\PhpSpreadsheet\Calculation\Token\Stack; use PhpOffice\PhpSpreadsheet\Cell\AddressRange; use PhpOffice\PhpSpreadsheet\Cell\Cell; @@ -4632,14 +4630,6 @@ private static function dataTestReference(&$operandData) return $operand; } - private const NUMERIC_BINARY_OPERATIONS = [ - '+' => 'plusEquals', - '-' => 'minusEquals', - '*' => 'arrayTimesEquals', - '/' => 'arrayRightDivide', - '^' => 'power', - ]; - /** * @param mixed $tokens * @param null|string $cellID @@ -4679,7 +4669,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null) if ( (isset($storeValue) || $tokenData['reference'] === 'NULL') - && (!$storeValueAsBool || ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) + && (!$storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) ) { // If branching value is not true, we don't need to compute if (!isset($fakedForBranchPruning['onlyIf-' . $onlyIfStoreKey])) { @@ -4711,7 +4701,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null) if ( (isset($storeValue) || $tokenData['reference'] === 'NULL') - && ($storeValueAsBool || ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) + && ($storeValueAsBool || Information\ErrorValue::isError($storeValue) || ($storeValue === 'Pruned branch')) ) { // If branching value is true, we don't need to compute if (!isset($fakedForBranchPruning['onlyIfNot-' . $onlyIfNotStoreKey])) { @@ -4857,7 +4847,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null) case '*': // Multiplication case '/': // Division case '^': // Exponential - $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, self::NUMERIC_BINARY_OPERATIONS[$token], $stack); + $result = $this->executeNumericBinaryOperation($operand1, $operand2, $token, $stack); if (isset($storeKey)) { $branchStore[$storeKey] = $result; } @@ -4867,29 +4857,30 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null) // If either of the operands is a matrix, we need to treat them both as matrices // (converting the other operand to a matrix if need be); then perform the required // matrix operation - if (is_bool($operand1)) { - $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE']; - } - if (is_bool($operand2)) { - $operand2 = ($operand2) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE']; - } - if ((is_array($operand1)) || (is_array($operand2))) { + $operand1 = self::boolToString($operand1); + $operand2 = self::boolToString($operand2); + if (is_array($operand1) || is_array($operand2)) { + if (is_string($operand1)) { + $operand1 = self::unwrapResult($operand1); + } + if (is_string($operand2)) { + $operand2 = self::unwrapResult($operand2); + } // Ensure that both operands are arrays/matrices - self::checkMatrixOperands($operand1, $operand2, 2); - - try { - // Convert operand 1 from a PHP array to a matrix - $matrix = new Shared\JAMA\Matrix($operand1); - // 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); + [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2); + + for ($row = 0; $row < $rows; ++$row) { + for ($column = 0; $column < $columns; ++$column) { + $operand1[$row][$column] = + Shared\StringHelper::substring( + self::boolToString($operand1[$row][$column]) + . self::boolToString($operand2[$row][$column]), + 0, + DataType::MAX_STRING_LENGTH + ); } - } catch (\Exception $ex) { - $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); - $result = '#VALUE!'; } + $result = $operand1; } else { // In theory, we should truncate here. // But I can't figure out a formula @@ -4942,23 +4933,26 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null) $multiplier = 0.01; } if (is_array($arg)) { - self::checkMatrixOperands($arg, $multiplier, 2); - - try { - $matrix1 = new Shared\JAMA\Matrix($arg); - $matrixResult = $matrix1->arrayTimesEquals($multiplier); - $result = $matrixResult->getArray(); - } catch (\Exception $ex) { - $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); - $result = '#VALUE!'; + $operand2 = $multiplier; + $result = $arg; + [$rows, $columns] = self::checkMatrixOperands($result, $operand2, 0); + for ($row = 0; $row < $rows; ++$row) { + for ($column = 0; $column < $columns; ++$column) { + if (is_numeric($result[$row][$column])) { + $result[$row][$column] *= $multiplier; + } else { + $result[$row][$column] = Information\ExcelError::VALUE(); + } + } } + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails($result)); $stack->push('Value', $result); if (isset($storeKey)) { $branchStore[$storeKey] = $result; } } else { - $this->executeNumericBinaryOperation($multiplier, $arg, '*', 'arrayTimesEquals', $stack); + $this->executeNumericBinaryOperation($multiplier, $arg, '*', $stack); } } elseif (preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '$/i', $token ?? '', $matches)) { $cellRef = null; @@ -5298,12 +5292,11 @@ private function executeBinaryComparisonOperation($operand1, $operand2, $operati * @param mixed $operand1 * @param mixed $operand2 * @param string $operation - * @param string $matrixFunction * @param Stack $stack * * @return bool|mixed */ - private function executeNumericBinaryOperation($operand1, $operand2, $operation, $matrixFunction, &$stack) + private function executeNumericBinaryOperation($operand1, $operand2, $operation, &$stack) { // Validate the two operands if ( @@ -5313,70 +5306,113 @@ private function executeNumericBinaryOperation($operand1, $operand2, $operation, return false; } - // If either of the operands is a matrix, we need to treat them both as matrices - // (converting the other operand to a matrix if need be); then perform the required - // matrix operation - if ((is_array($operand1)) || (is_array($operand2))) { - // Ensure that both operands are arrays/matrices of the same size - self::checkMatrixOperands($operand1, $operand2, 2); - - try { - // Convert operand 1 from a PHP array to a matrix - $matrix = new Shared\JAMA\Matrix($operand1); - // Perform the required operation against the operand 1 matrix, passing in operand 2 - $matrixResult = $matrix->$matrixFunction($operand2); - $result = $matrixResult->getArray(); - } catch (\Exception $ex) { - $this->debugLog->writeDebugLog('JAMA Matrix Exception: %s', $ex->getMessage()); - $result = '#VALUE!'; + if ( + (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) && + ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) || + (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0)) + ) { + $result = Information\ExcelError::VALUE(); + } elseif (is_array($operand1) || is_array($operand2)) { + // Ensure that both operands are arrays/matrices + if (is_array($operand1)) { + foreach ($operand1 as $key => $value) { + $operand1[$key] = Functions::flattenArray($value); + } } - } else { - if ( - (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) && - ((is_string($operand1) && !is_numeric($operand1) && strlen($operand1) > 0) || - (is_string($operand2) && !is_numeric($operand2) && strlen($operand2) > 0)) - ) { - $result = Information\ExcelError::VALUE(); - } else { - // If we're dealing with non-matrix operations, execute the necessary operation - switch ($operation) { - // Addition - case '+': - $result = $operand1 + $operand2; + if (is_array($operand2)) { + foreach ($operand2 as $key => $value) { + $operand2[$key] = Functions::flattenArray($value); + } + } + [$rows, $columns] = self::checkMatrixOperands($operand1, $operand2, 2); - break; - // Subtraction - case '-': - $result = $operand1 - $operand2; + for ($row = 0; $row < $rows; ++$row) { + for ($column = 0; $column < $columns; ++$column) { + if ($operand1[$row][$column] === null) { + $operand1[$row][$column] = 0; + } elseif (!is_numeric($operand1[$row][$column])) { + $operand1[$row][$column] = Information\ExcelError::VALUE(); - break; - // Multiplication - case '*': - $result = $operand1 * $operand2; + continue; + } + if ($operand2[$row][$column] === null) { + $operand2[$row][$column] = 0; + } elseif (!is_numeric($operand2[$row][$column])) { + $operand1[$row][$column] = Information\ExcelError::VALUE(); - break; - // Division - case '/': - if ($operand2 == 0) { - // Trap for Divide by Zero error - $stack->push('Error', ExcelError::DIV0()); - $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails(ExcelError::DIV0())); - - return false; - } - $result = $operand1 / $operand2; + continue; + } + switch ($operation) { + case '+': + $operand1[$row][$column] += $operand2[$row][$column]; + + break; + case '-': + $operand1[$row][$column] -= $operand2[$row][$column]; + + break; + case '*': + $operand1[$row][$column] *= $operand2[$row][$column]; + + break; + case '/': + if ($operand2[$row][$column] == 0) { + $operand1[$row][$column] = Information\ExcelError::DIV0(); + } else { + $operand1[$row][$column] /= $operand2[$row][$column]; + } - break; - // Power - case '^': - $result = $operand1 ** $operand2; + break; + case '^': + $operand1[$row][$column] = $operand1[$row][$column] ** $operand2[$row][$column]; - break; + break; - default: - throw new Exception('Unsupported numeric binary operation'); + default: + throw new Exception('Unsupported numeric binary operation'); + } } } + $result = $operand1; + } else { + // If we're dealing with non-matrix operations, execute the necessary operation + switch ($operation) { + // Addition + case '+': + $result = $operand1 + $operand2; + + break; + // Subtraction + case '-': + $result = $operand1 - $operand2; + + break; + // Multiplication + case '*': + $result = $operand1 * $operand2; + + break; + // Division + case '/': + if ($operand2 == 0) { + // Trap for Divide by Zero error + $stack->push('Error', Information\ExcelError::DIV0()); + $this->debugLog->writeDebugLog('Evaluation Result is %s', $this->showTypeDetails(Information\ExcelError::DIV0())); + + return false; + } + $result = $operand1 / $operand2; + + break; + // Power + case '^': + $result = $operand1 ** $operand2; + + break; + + default: + throw new Exception('Unsupported numeric binary operation'); + } } // Log the result details @@ -5638,25 +5674,6 @@ private function addCellReference(array $args, $passCellReference, $functionCall return $args; } - /** - * @param array $tokens - * - * @return string - */ - private function getTokensAsString($tokens) - { - $tokensStr = array_map(function ($token) { - $value = $token['value'] ?? 'no value'; - while (is_array($value)) { - $value = array_pop($value); - } - - return $value; - }, $tokens); - - return '[ ' . implode(' | ', $tokensStr) . ' ]'; - } - /** * @return mixed|string */ @@ -5725,4 +5742,20 @@ private static function doNothing($arg): bool { return (bool) $arg; } + + /** + * @param mixed $operand1 + * + * @return mixed + */ + private static function boolToString($operand1) + { + if (is_bool($operand1)) { + $operand1 = ($operand1) ? self::$localeBoolean['TRUE'] : self::$localeBoolean['FALSE']; + } elseif ($operand1 === null) { + $operand1 = ''; + } + + return $operand1; + } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php b/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php index 3f8436ffdc..304b44b854 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Maximum.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; class Maximum extends MaxMinBase { @@ -26,6 +27,11 @@ public static function max(...$args) // Loop through arguments $aArgs = Functions::flattenArray($args); foreach ($aArgs as $arg) { + if (ErrorValue::isError($arg)) { + $returnValue = $arg; + + break; + } // Is it a numeric value? if ((is_numeric($arg)) && (!is_string($arg))) { if (($returnValue === null) || ($arg > $returnValue)) { @@ -60,6 +66,11 @@ public static function maxA(...$args) // Loop through arguments $aArgs = Functions::flattenArray($args); foreach ($aArgs as $arg) { + if (ErrorValue::isError($arg)) { + $returnValue = $arg; + + break; + } // Is it a numeric value? if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { $arg = self::datatypeAdjustmentAllowStrings($arg); diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php b/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php index 596aad78fd..0dbbf3a3e9 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue; class Minimum extends MaxMinBase { @@ -26,6 +27,11 @@ public static function min(...$args) // Loop through arguments $aArgs = Functions::flattenArray($args); foreach ($aArgs as $arg) { + if (ErrorValue::isError($arg)) { + $returnValue = $arg; + + break; + } // Is it a numeric value? if ((is_numeric($arg)) && (!is_string($arg))) { if (($returnValue === null) || ($arg < $returnValue)) { @@ -60,6 +66,11 @@ public static function minA(...$args) // Loop through arguments $aArgs = Functions::flattenArray($args); foreach ($aArgs as $arg) { + if (ErrorValue::isError($arg)) { + $returnValue = $arg; + + break; + } // Is it a numeric value? if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { $arg = self::datatypeAdjustmentAllowStrings($arg); diff --git a/src/PhpSpreadsheet/Shared/JAMA/CHANGELOG.TXT b/src/PhpSpreadsheet/Shared/JAMA/CHANGELOG.TXT deleted file mode 100644 index 1c18a5da35..0000000000 --- a/src/PhpSpreadsheet/Shared/JAMA/CHANGELOG.TXT +++ /dev/null @@ -1,16 +0,0 @@ -Mar 1, 2005 11:15 AST by PM - -+ For consistency, renamed Math.php to Maths.java, utils to util, - tests to test, docs to doc - - -+ Removed conditional logic from top of Matrix class. - -+ Switched to using hypo function in Maths.php for all php-hypot calls. - NOTE TO SELF: Need to make sure that all decompositions have been - switched over to using the bundled hypo. - -Feb 25, 2005 at 10:00 AST by PM - -+ Recommend using simpler Error.php instead of JAMA_Error.php but - can be persuaded otherwise. - diff --git a/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php deleted file mode 100644 index cc09b64d7f..0000000000 --- a/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php +++ /dev/null @@ -1,286 +0,0 @@ -= n, the LU decomposition is an m-by-n - * unit lower triangular matrix L, an n-by-n upper triangular matrix U, - * and a permutation vector piv of length m so that A(piv,:) = L*U. - * If m < n, then L is m-by-m and U is m-by-n. - * - * The LU decompostion with pivoting always exists, even if the matrix is - * singular, so the constructor will never fail. The primary use of the - * LU decomposition is in the solution of square systems of simultaneous - * linear equations. This will fail if isNonsingular() returns false. - * - * @author Paul Meagher - * @author Bartosz Matosiuk - * @author Michael Bommarito - * - * @version 1.1 - */ -class LUDecomposition -{ - const MATRIX_SINGULAR_EXCEPTION = 'Can only perform operation on singular matrix.'; - const MATRIX_SQUARE_EXCEPTION = 'Mismatched Row dimension'; - - /** - * Decomposition storage. - * - * @var array - */ - private $LU = []; - - /** - * Row dimension. - * - * @var int - */ - private $m; - - /** - * Column dimension. - * - * @var int - */ - private $n; - - /** - * Pivot sign. - * - * @var int - */ - private $pivsign; - - /** - * Internal storage of pivot vector. - * - * @var array - */ - private $piv = []; - - /** - * LU Decomposition constructor. - * - * @param ?Matrix $A Rectangular matrix - */ - public function __construct($A) - { - if ($A instanceof Matrix) { - // Use a "left-looking", dot-product, Crout/Doolittle algorithm. - $this->LU = $A->getArray(); - $this->m = $A->getRowDimension(); - $this->n = $A->getColumnDimension(); - for ($i = 0; $i < $this->m; ++$i) { - $this->piv[$i] = $i; - } - $this->pivsign = 1; - $LUcolj = []; - - // Outer loop. - for ($j = 0; $j < $this->n; ++$j) { - // Make a copy of the j-th column to localize references. - for ($i = 0; $i < $this->m; ++$i) { - $LUcolj[$i] = &$this->LU[$i][$j]; - } - // Apply previous transformations. - for ($i = 0; $i < $this->m; ++$i) { - $LUrowi = $this->LU[$i]; - // Most of the time is spent in the following dot product. - $kmax = min($i, $j); - $s = 0.0; - for ($k = 0; $k < $kmax; ++$k) { - $s += $LUrowi[$k] * $LUcolj[$k]; - } - $LUrowi[$j] = $LUcolj[$i] -= $s; - } - // Find pivot and exchange if necessary. - $p = $j; - for ($i = $j + 1; $i < $this->m; ++$i) { - if (abs($LUcolj[$i]) > abs($LUcolj[$p])) { - $p = $i; - } - } - if ($p != $j) { - for ($k = 0; $k < $this->n; ++$k) { - $t = $this->LU[$p][$k]; - $this->LU[$p][$k] = $this->LU[$j][$k]; - $this->LU[$j][$k] = $t; - } - $k = $this->piv[$p]; - $this->piv[$p] = $this->piv[$j]; - $this->piv[$j] = $k; - $this->pivsign = $this->pivsign * -1; - } - // Compute multipliers. - if (($j < $this->m) && ($this->LU[$j][$j] != 0.0)) { - for ($i = $j + 1; $i < $this->m; ++$i) { - $this->LU[$i][$j] /= $this->LU[$j][$j]; - } - } - } - } else { - throw new CalculationException(Matrix::ARGUMENT_TYPE_EXCEPTION); - } - } - - // function __construct() - - /** - * Get lower triangular factor. - * - * @return Matrix Lower triangular factor - */ - public function getL() - { - $L = []; - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - if ($i > $j) { - $L[$i][$j] = $this->LU[$i][$j]; - } elseif ($i == $j) { - $L[$i][$j] = 1.0; - } else { - $L[$i][$j] = 0.0; - } - } - } - - return new Matrix($L); - } - - // function getL() - - /** - * Get upper triangular factor. - * - * @return Matrix Upper triangular factor - */ - public function getU() - { - $U = []; - for ($i = 0; $i < $this->n; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - if ($i <= $j) { - $U[$i][$j] = $this->LU[$i][$j]; - } else { - $U[$i][$j] = 0.0; - } - } - } - - return new Matrix($U); - } - - // function getU() - - /** - * Return pivot permutation vector. - * - * @return array Pivot vector - */ - public function getPivot() - { - return $this->piv; - } - - // function getPivot() - - /** - * Alias for getPivot. - * - * @see getPivot - * - * @return array Pivot vector - */ - public function getDoublePivot() - { - return $this->getPivot(); - } - - // function getDoublePivot() - - /** - * Is the matrix nonsingular? - * - * @return bool true if U, and hence A, is nonsingular - */ - public function isNonsingular() - { - for ($j = 0; $j < $this->n; ++$j) { - if ($this->LU[$j][$j] == 0) { - return false; - } - } - - return true; - } - - // function isNonsingular() - - /** - * Count determinants. - * - * @return float - */ - public function det() - { - if ($this->m == $this->n) { - $d = $this->pivsign; - for ($j = 0; $j < $this->n; ++$j) { - $d *= $this->LU[$j][$j]; - } - - return $d; - } - - throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION); - } - - // function det() - - /** - * Solve A*X = B. - * - * @param Matrix $B a Matrix with as many rows as A and any number of columns - * - * @return Matrix X so that L*U*X = B(piv,:) - */ - public function solve(Matrix $B) - { - if ($B->getRowDimension() == $this->m) { - if ($this->isNonsingular()) { - // Copy right hand side with pivoting - $nx = $B->getColumnDimension(); - $X = $B->getMatrix($this->piv, 0, $nx - 1); - // Solve L*Y = B(piv,:) - for ($k = 0; $k < $this->n; ++$k) { - for ($i = $k + 1; $i < $this->n; ++$i) { - for ($j = 0; $j < $nx; ++$j) { - $X->A[$i][$j] -= $X->A[$k][$j] * $this->LU[$i][$k]; - } - } - } - // Solve U*X = Y; - for ($k = $this->n - 1; $k >= 0; --$k) { - for ($j = 0; $j < $nx; ++$j) { - $X->A[$k][$j] /= $this->LU[$k][$k]; - } - for ($i = 0; $i < $k; ++$i) { - for ($j = 0; $j < $nx; ++$j) { - $X->A[$i][$j] -= $X->A[$k][$j] * $this->LU[$i][$k]; - } - } - } - - return $X; - } - - throw new CalculationException(self::MATRIX_SINGULAR_EXCEPTION); - } - - throw new CalculationException(self::MATRIX_SQUARE_EXCEPTION); - } -} diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php deleted file mode 100644 index 75e44240ed..0000000000 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ /dev/null @@ -1,1167 +0,0 @@ - 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - //Rectangular matrix - m x n initialized from 2D array - case 'array': - $this->m = count($args[0]); - $this->n = count($args[0][0]); - $this->A = $args[0]; - - break; - //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 - 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 - case 'array,integer': - $this->m = $args[1]; - if ($this->m != 0) { - $this->n = count($args[0]) / $this->m; - } else { - $this->n = 0; - } - if (($this->m * $this->n) == count($args[0])) { - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $this->A[$i][$j] = $args[0][$i + $j * $this->m]; - } - } - } else { - throw new CalculationException(self::ARRAY_LENGTH_EXCEPTION); - } - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - } else { - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - } - - /** - * getArray. - * - * @return array Matrix array - */ - public function getArray() - { - return $this->A; - } - - /** - * getRowDimension. - * - * @return int Row dimension - */ - public function getRowDimension() - { - return $this->m; - } - - /** - * getColumnDimension. - * - * @return int Column dimension - */ - public function getColumnDimension() - { - return $this->n; - } - - /** - * get. - * - * Get the i,j-th element of the matrix. - * - * @param int $i Row position - * @param int $j Column position - * - * @return float|int - */ - public function get($i = null, $j = null) - { - return $this->A[$i][$j]; - } - - /** - * getMatrix. - * - * Get a submatrix - * - * @return Matrix Submatrix - */ - public function getMatrix(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - //A($i0...; $j0...) - case 'integer,integer': - [$i0, $j0] = $args; - if ($i0 >= 0) { - $m = $this->m - $i0; - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - if ($j0 >= 0) { - $n = $this->n - $j0; - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - $R = new self($m, $n); - for ($i = $i0; $i < $this->m; ++$i) { - for ($j = $j0; $j < $this->n; ++$j) { - $R->set($i, $j, $this->A[$i][$j]); - } - } - - return $R; - //A($i0...$iF; $j0...$jF) - case 'integer,integer,integer,integer': - [$i0, $iF, $j0, $jF] = $args; - if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { - $m = $iF - $i0; - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - if (($jF > $j0) && ($this->n >= $jF) && ($j0 >= 0)) { - $n = $jF - $j0; - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - $R = new self($m + 1, $n + 1); - for ($i = $i0; $i <= $iF; ++$i) { - for ($j = $j0; $j <= $jF; ++$j) { - $R->set($i - $i0, $j - $j0, $this->A[$i][$j]); - } - } - - return $R; - //$R = array of row indices; $C = array of column indices - case 'array,array': - [$RL, $CL] = $args; - if (count($RL) > 0) { - $m = count($RL); - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - if (count($CL) > 0) { - $n = count($CL); - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - $R = new self($m, $n); - for ($i = 0; $i < $m; ++$i) { - for ($j = 0; $j < $n; ++$j) { - $R->set($i, $j, $this->A[$RL[$i]][$CL[$j]]); - } - } - - return $R; - //A($i0...$iF); $CL = array of column indices - case 'integer,integer,array': - [$i0, $iF, $CL] = $args; - if (($iF > $i0) && ($this->m >= $iF) && ($i0 >= 0)) { - $m = $iF - $i0; - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - if (count($CL) > 0) { - $n = count($CL); - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - $R = new self($m, $n); - for ($i = $i0; $i < $iF; ++$i) { - for ($j = 0; $j < $n; ++$j) { - $R->set($i - $i0, $j, $this->A[$i][$CL[$j]]); - } - } - - return $R; - //$RL = array of row indices - case 'array,integer,integer': - [$RL, $j0, $jF] = $args; - if (count($RL) > 0) { - $m = count($RL); - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - if (($jF >= $j0) && ($this->n >= $jF) && ($j0 >= 0)) { - $n = $jF - $j0; - } else { - throw new CalculationException(self::ARGUMENT_BOUNDS_EXCEPTION); - } - $R = new self($m, $n + 1); - for ($i = 0; $i < $m; ++$i) { - for ($j = $j0; $j <= $jF; ++$j) { - $R->set($i, $j - $j0, $this->A[$RL[$i]][$j]); - } - } - - return $R; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - } else { - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - } - - /** - * checkMatrixDimensions. - * - * Is matrix B the same size? - * - * @param Matrix $B Matrix B - * - * @return bool - */ - public function checkMatrixDimensions($B = null) - { - if ($B instanceof self) { - if (($this->m == $B->getRowDimension()) && ($this->n == $B->getColumnDimension())) { - return true; - } - - throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); - } - - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - // function checkMatrixDimensions() - - /** - * set. - * - * Set the i,j-th element of the matrix. - * - * @param int $i Row position - * @param int $j Column position - * @param float|int $c value - */ - public function set($i = null, $j = null, $c = null): void - { - // Optimized set version just has this - $this->A[$i][$j] = $c; - } - - // function set() - - /** - * identity. - * - * Generate an identity matrix. - * - * @param int $m Row dimension - * @param int $n Column dimension - * - * @return Matrix Identity matrix - */ - public function identity($m = null, $n = null) - { - return $this->diagonal($m, $n, 1); - } - - /** - * diagonal. - * - * Generate a diagonal matrix - * - * @param int $m Row dimension - * @param int $n Column dimension - * @param mixed $c Diagonal value - * - * @return Matrix Diagonal matrix - */ - public function diagonal($m = null, $n = null, $c = 1) - { - $R = new self($m, $n); - for ($i = 0; $i < $m; ++$i) { - $R->set($i, $i, $c); - } - - return $R; - } - - /** - * getMatrixByRow. - * - * Get a submatrix by row index/range - * - * @param int $i0 Initial row index - * @param int $iF Final row index - * - * @return Matrix Submatrix - */ - public function getMatrixByRow($i0 = null, $iF = null) - { - if (is_int($i0)) { - if (is_int($iF)) { - return $this->getMatrix($i0, 0, $iF + 1, $this->n); - } - - return $this->getMatrix($i0, 0, $i0 + 1, $this->n); - } - - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - /** - * getMatrixByCol. - * - * Get a submatrix by column index/range - * - * @param int $j0 Initial column index - * @param int $jF Final column index - * - * @return Matrix Submatrix - */ - public function getMatrixByCol($j0 = null, $jF = null) - { - if (is_int($j0)) { - if (is_int($jF)) { - return $this->getMatrix(0, $j0, $this->m, $jF + 1); - } - - return $this->getMatrix(0, $j0, $this->m, $j0 + 1); - } - - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - /** - * transpose. - * - * Tranpose matrix - * - * @return Matrix Transposed matrix - */ - public function transpose() - { - $R = new self($this->n, $this->m); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $R->set($j, $i, $this->A[$i][$j]); - } - } - - return $R; - } - - // function transpose() - - /** - * trace. - * - * Sum of diagonal elements - * - * @return float Sum of diagonal elements - */ - public function trace() - { - $s = 0; - $n = min($this->m, $this->n); - for ($i = 0; $i < $n; ++$i) { - $s += $this->A[$i][$i]; - } - - return $s; - } - - /** - * plus. - * - * A + B - * - * @return Matrix Sum - */ - public function plus(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $M->set($i, $j, $M->get($i, $j) + $this->A[$i][$j]); - } - } - - return $M; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * plusEquals. - * - * A = A + B - * - * @return $this - */ - public function plusEquals(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $validValues = true; - $value = $M->get($i, $j); - [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); - [$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues); - if ($validValues) { - $this->A[$i][$j] += $value; - } else { - $this->A[$i][$j] = ExcelError::NAN(); - } - } - } - - return $this; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * minus. - * - * A - B - * - * @return Matrix Sum - */ - public function minus(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $M->set($i, $j, $M->get($i, $j) - $this->A[$i][$j]); - } - } - - return $M; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * minusEquals. - * - * A = A - B - * - * @return $this - */ - public function minusEquals(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $validValues = true; - $value = $M->get($i, $j); - [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); - [$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues); - if ($validValues) { - $this->A[$i][$j] -= $value; - } else { - $this->A[$i][$j] = ExcelError::NAN(); - } - } - } - - return $this; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * arrayTimes. - * - * Element-by-element multiplication - * Cij = Aij * Bij - * - * @return Matrix Matrix Cij - */ - public function arrayTimes(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $M->set($i, $j, $M->get($i, $j) * $this->A[$i][$j]); - } - } - - return $M; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * arrayTimesEquals. - * - * Element-by-element multiplication - * Aij = Aij * Bij - * - * @return $this - */ - public function arrayTimesEquals(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $validValues = true; - $value = $M->get($i, $j); - [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); - [$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues); - if ($validValues) { - $this->A[$i][$j] *= $value; - } else { - $this->A[$i][$j] = ExcelError::NAN(); - } - } - } - - return $this; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * arrayRightDivide. - * - * Element-by-element right division - * A / B - * - * @return Matrix Division result - */ - public function arrayRightDivide(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $validValues = true; - $value = $M->get($i, $j); - [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); - [$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues); - if ($validValues) { - if ($value == 0) { - // Trap for Divide by Zero error - $M->set($i, $j, /** @scrutinizer ignore-type */ '#DIV/0!'); - } else { - $M->set($i, $j, $this->A[$i][$j] / $value); - } - } else { - $M->set($i, $j, /** @scrutinizer ignore-type */ ExcelError::NAN()); - } - } - } - - return $M; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * arrayRightDivideEquals. - * - * Element-by-element right division - * Aij = Aij / Bij - * - * @return Matrix Matrix Aij - */ - public function arrayRightDivideEquals(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $this->A[$i][$j] = $this->A[$i][$j] / $M->get($i, $j); - } - } - - return $M; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * arrayLeftDivide. - * - * Element-by-element Left division - * A / B - * - * @return Matrix Division result - */ - public function arrayLeftDivide(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $M->set($i, $j, $M->get($i, $j) / $this->A[$i][$j]); - } - } - - return $M; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * arrayLeftDivideEquals. - * - * Element-by-element Left division - * Aij = Aij / Bij - * - * @return Matrix Matrix Aij - */ - public function arrayLeftDivideEquals(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $this->A[$i][$j] = $M->get($i, $j) / $this->A[$i][$j]; - } - } - - return $M; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * times. - * - * Matrix multiplication - * - * @return Matrix Product - */ - public function times(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $B = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - if ($this->n == $B->m) { - $C = new self($this->m, $B->n); - for ($j = 0; $j < $B->n; ++$j) { - $Bcolj = []; - for ($k = 0; $k < $this->n; ++$k) { - $Bcolj[$k] = $B->A[$k][$j]; - } - for ($i = 0; $i < $this->m; ++$i) { - $Arowi = $this->A[$i]; - $s = 0; - for ($k = 0; $k < $this->n; ++$k) { - $s += $Arowi[$k] * $Bcolj[$k]; - } - $C->A[$i][$j] = $s; - } - } - - return $C; - } - - throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); - case 'array': - $B = new self($args[0]); - if ($this->n == $B->m) { - $C = new self($this->m, $B->n); - for ($i = 0; $i < $C->m; ++$i) { - for ($j = 0; $j < $C->n; ++$j) { - $s = '0'; - for ($k = 0; $k < $C->n; ++$k) { - $s += $this->A[$i][$k] * $B->A[$k][$j]; - } - $C->A[$i][$j] = $s; - } - } - - return $C; - } - - throw new CalculationException(self::MATRIX_DIMENSION_EXCEPTION); - case 'integer': - $C = new self($this->A); - for ($i = 0; $i < $C->m; ++$i) { - for ($j = 0; $j < $C->n; ++$j) { - $C->A[$i][$j] *= $args[0]; - } - } - - return $C; - case 'double': - $C = new self($this->m, $this->n); - for ($i = 0; $i < $C->m; ++$i) { - for ($j = 0; $j < $C->n; ++$j) { - $C->A[$i][$j] = $args[0] * $this->A[$i][$j]; - } - } - - return $C; - case 'float': - $C = new self($this->A); - for ($i = 0; $i < $C->m; ++$i) { - for ($j = 0; $j < $C->n; ++$j) { - $C->A[$i][$j] *= $args[0]; - } - } - - return $C; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - } else { - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - } - - /** - * power. - * - * A = A ^ B - * - * @return $this - */ - public function power(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $this->checkMatrixDimensions($M); - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - $validValues = true; - $value = $M->get($i, $j); - [$this->A[$i][$j], $validValues] = $this->validateExtractedValue($this->A[$i][$j], $validValues); - [$value, $validValues] = $this->validateExtractedValue($value, /** @scrutinizer ignore-type */ $validValues); - if ($validValues) { - $this->A[$i][$j] = $this->A[$i][$j] ** $value; - } else { - $this->A[$i][$j] = ExcelError::NAN(); - } - } - } - - return $this; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * concat. - * - * A = A & B - * - * @return $this - */ - public function concat(...$args) - { - if (count($args) > 0) { - $match = implode(',', array_map('gettype', $args)); - - switch ($match) { - case 'object': - if ($args[0] instanceof self) { - $M = $args[0]; - } else { - throw new CalculationException(self::ARGUMENT_TYPE_EXCEPTION); - } - - break; - case 'array': - $M = new self($args[0]); - - break; - default: - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - - break; - } - $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), '"'); - } - } - - return $this; - } - - throw new CalculationException(self::POLYMORPHIC_ARGUMENT_EXCEPTION); - } - - /** - * Solve A*X = B. - * - * @param Matrix $B Right hand side - * - * @return Matrix ... Solution if A is square, least squares solution otherwise - */ - public function solve(self $B) - { - if ($this->m == $this->n) { - $LU = new LUDecomposition($this); - - return $LU->solve($B); - } - $QR = new QRDecomposition($this); - - return $QR->solve($B); - } - - /** - * Matrix inverse or pseudoinverse. - * - * @return Matrix ... Inverse(A) if A is square, pseudoinverse otherwise. - */ - public function inverse() - { - return $this->solve($this->identity($this->m, $this->m)); - } - - /** - * det. - * - * Calculate determinant - * - * @return float Determinant - */ - public function det() - { - $L = new LUDecomposition($this); - - 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 &= FormattedNumber::convertToNumberIfFormatted($value); - } - - return [$value, $validValues]; - } -} diff --git a/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php deleted file mode 100644 index 9b51f41390..0000000000 --- a/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php +++ /dev/null @@ -1,245 +0,0 @@ -= n, the QR decomposition is an m-by-n - * orthogonal matrix Q and an n-by-n upper triangular matrix R so that - * A = Q*R. - * - * The QR decompostion always exists, even if the matrix does not have - * full rank, so the constructor will never fail. The primary use of the - * QR decomposition is in the least squares solution of nonsquare systems - * of simultaneous linear equations. This will fail if isFullRank() - * returns false. - * - * @author Paul Meagher - * - * @version 1.1 - */ -class QRDecomposition -{ - const MATRIX_RANK_EXCEPTION = 'Can only perform operation on full-rank matrix.'; - - /** - * Array for internal storage of decomposition. - * - * @var array - */ - private $QR = []; - - /** - * Row dimension. - * - * @var int - */ - private $m; - - /** - * Column dimension. - * - * @var int - */ - private $n; - - /** - * Array for internal storage of diagonal of R. - * - * @var array - */ - private $Rdiag = []; - - /** - * QR Decomposition computed by Householder reflections. - * - * @param Matrix $A Rectangular matrix - */ - public function __construct(Matrix $A) - { - // Initialize. - $this->QR = $A->getArray(); - $this->m = $A->getRowDimension(); - $this->n = $A->getColumnDimension(); - // Main loop. - for ($k = 0; $k < $this->n; ++$k) { - // Compute 2-norm of k-th column without under/overflow. - $nrm = 0.0; - for ($i = $k; $i < $this->m; ++$i) { - $nrm = hypo($nrm, $this->QR[$i][$k]); - } - if ($nrm != 0.0) { - // Form k-th Householder vector. - if ($this->QR[$k][$k] < 0) { - $nrm = -$nrm; - } - for ($i = $k; $i < $this->m; ++$i) { - $this->QR[$i][$k] /= $nrm; - } - $this->QR[$k][$k] += 1.0; - // Apply transformation to remaining columns. - for ($j = $k + 1; $j < $this->n; ++$j) { - $s = 0.0; - for ($i = $k; $i < $this->m; ++$i) { - $s += $this->QR[$i][$k] * $this->QR[$i][$j]; - } - $s = -$s / $this->QR[$k][$k]; - for ($i = $k; $i < $this->m; ++$i) { - $this->QR[$i][$j] += $s * $this->QR[$i][$k]; - } - } - } - $this->Rdiag[$k] = -$nrm; - } - } - - // function __construct() - - /** - * Is the matrix full rank? - * - * @return bool true if R, and hence A, has full rank, else false - */ - public function isFullRank() - { - for ($j = 0; $j < $this->n; ++$j) { - if ($this->Rdiag[$j] == 0) { - return false; - } - } - - return true; - } - - // function isFullRank() - - /** - * Return the Householder vectors. - * - * @return Matrix Lower trapezoidal matrix whose columns define the reflections - */ - public function getH() - { - $H = []; - for ($i = 0; $i < $this->m; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - if ($i >= $j) { - $H[$i][$j] = $this->QR[$i][$j]; - } else { - $H[$i][$j] = 0.0; - } - } - } - - return new Matrix($H); - } - - // function getH() - - /** - * Return the upper triangular factor. - * - * @return Matrix upper triangular factor - */ - public function getR() - { - $R = []; - for ($i = 0; $i < $this->n; ++$i) { - for ($j = 0; $j < $this->n; ++$j) { - if ($i < $j) { - $R[$i][$j] = $this->QR[$i][$j]; - } elseif ($i == $j) { - $R[$i][$j] = $this->Rdiag[$i]; - } else { - $R[$i][$j] = 0.0; - } - } - } - - return new Matrix($R); - } - - // function getR() - - /** - * Generate and return the (economy-sized) orthogonal factor. - * - * @return Matrix orthogonal factor - */ - public function getQ() - { - $Q = []; - for ($k = $this->n - 1; $k >= 0; --$k) { - for ($i = 0; $i < $this->m; ++$i) { - $Q[$i][$k] = 0.0; - } - $Q[$k][$k] = 1.0; - for ($j = $k; $j < $this->n; ++$j) { - if ($this->QR[$k][$k] != 0) { - $s = 0.0; - for ($i = $k; $i < $this->m; ++$i) { - $s += $this->QR[$i][$k] * $Q[$i][$j]; - } - $s = -$s / $this->QR[$k][$k]; - for ($i = $k; $i < $this->m; ++$i) { - $Q[$i][$j] += $s * $this->QR[$i][$k]; - } - } - } - } - - return new Matrix($Q); - } - - // function getQ() - - /** - * Least squares solution of A*X = B. - * - * @param Matrix $B a Matrix with as many rows as A and any number of columns - * - * @return Matrix matrix that minimizes the two norm of Q*R*X-B - */ - public function solve(Matrix $B) - { - if ($B->getRowDimension() == $this->m) { - if ($this->isFullRank()) { - // Copy right hand side - $nx = $B->getColumnDimension(); - $X = $B->getArray(); - // Compute Y = transpose(Q)*B - for ($k = 0; $k < $this->n; ++$k) { - for ($j = 0; $j < $nx; ++$j) { - $s = 0.0; - for ($i = $k; $i < $this->m; ++$i) { - $s += $this->QR[$i][$k] * $X[$i][$j]; - } - $s = -$s / $this->QR[$k][$k]; - for ($i = $k; $i < $this->m; ++$i) { - $X[$i][$j] += $s * $this->QR[$i][$k]; - } - } - } - // Solve R*X = Y; - for ($k = $this->n - 1; $k >= 0; --$k) { - for ($j = 0; $j < $nx; ++$j) { - $X[$k][$j] /= $this->Rdiag[$k]; - } - for ($i = 0; $i < $k; ++$i) { - for ($j = 0; $j < $nx; ++$j) { - $X[$i][$j] -= $X[$k][$j] * $this->QR[$i][$k]; - } - } - } - $X = new Matrix($X); - - return $X->getMatrix(0, $this->n - 1, 0, $nx); - } - - throw new CalculationException(self::MATRIX_RANK_EXCEPTION); - } - - throw new CalculationException(Matrix::MATRIX_DIMENSION_EXCEPTION); - } -} diff --git a/src/PhpSpreadsheet/Shared/JAMA/utils/Maths.php b/src/PhpSpreadsheet/Shared/JAMA/utils/Maths.php deleted file mode 100644 index 49877b23ea..0000000000 --- a/src/PhpSpreadsheet/Shared/JAMA/utils/Maths.php +++ /dev/null @@ -1,31 +0,0 @@ - abs($b)) { - $r = $b / $a; - $r = abs($a) * sqrt(1 + $r * $r); - } elseif ($b != 0) { - $r = $a / $b; - $r = abs($b) * sqrt(1 + $r * $r); - } else { - $r = 0.0; - } - - return $r; -} diff --git a/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php b/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php index ea40e29673..a216359669 100644 --- a/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared\Trend; -use PhpOffice\PhpSpreadsheet\Shared\JAMA\Matrix; +use Matrix\Matrix; // Phpstan and Scrutinizer seem to have legitimate complaints. // $this->slope is specified where an array is expected in several places. @@ -167,8 +167,8 @@ private function polynomialRegression($order, $yValues, $xValues): void $C = $matrixA->solve($matrixB); $coefficients = []; - for ($i = 0; $i < $C->getRowDimension(); ++$i) { - $r = $C->get($i, 0); + for ($i = 0; $i < $C->rows; ++$i) { + $r = $C->getValue($i + 1, 1); // row and column are origin-1 if (abs($r) <= 10 ** (-9)) { $r = 0; } diff --git a/tests/PhpSpreadsheetTests/Calculation/ArrayFormulaTest.php b/tests/PhpSpreadsheetTests/Calculation/ArrayFormulaTest.php index ef8c398248..0f8d8b3311 100644 --- a/tests/PhpSpreadsheetTests/Calculation/ArrayFormulaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/ArrayFormulaTest.php @@ -3,36 +3,11 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class ArrayFormulaTest extends TestCase { - /** - * @var string - */ - private $compatibilityMode; - - /** - * @var string - */ - private $locale; - - protected function setUp(): void - { - $this->compatibilityMode = Functions::getCompatibilityMode(); - $calculation = Calculation::getInstance(); - $this->locale = $calculation->getLocale(); - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - - protected function tearDown(): void - { - Functions::setCompatibilityMode($this->compatibilityMode); - $calculation = Calculation::getInstance(); - $calculation->setLocale($this->locale); - } - /** * @dataProvider providerArrayFormulae * @@ -51,6 +26,10 @@ public function providerArrayFormulae(): array '=MAX(ABS({-3, 4, -2; 6, -3, -12}))', 12, ], + 'unary operator applied to function' => [ + '=MAX(-ABS({-3, 4, -2; 6, -3, -12}))', + -2, + ], [ '=SUM(SEQUENCE(3,3,0,1))', 36, @@ -71,6 +50,38 @@ public function providerArrayFormulae(): array '=IFS(FALSE, {1,2,3}, TRUE, {4,5,6})', [[4, 5, 6]], ], + 'some invalid values' => [ + '=ABS({1,-2,"X3"; "B4",5,6})', + [[1, 2, '#VALUE!'], ['#VALUE!', 5, 6]], + ], + 'some invalid values with unary minus' => [ + '=-({1,-2,"X3"; "B4",5,6})', + [[-1, 2, '#VALUE!'], ['#VALUE!', -5, -6]], + ], ]; } + + public function testArrayFormulaUsingCells(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A4')->setValue(-3); + $sheet->getCell('B4')->setValue(4); + $sheet->getCell('C4')->setValue(-2); + $sheet->getCell('A5')->setValue(6); + $sheet->getCell('B5')->setValue(-3); + $sheet->getCell('C5')->setValue(-12); + $sheet->getCell('E4')->setValue('=MAX(-ABS(A4:C5))'); + self::assertSame(-2, $sheet->getCell('E4')->getCalculatedValue()); + $sheet->getCell('C4')->setValue('XYZ'); + $sheet->getCell('F4')->setValue('=MAX(-ABS(A4:C5))'); + self::assertSame('#VALUE!', $sheet->getCell('F4')->getCalculatedValue()); + $sheet->getCell('G4')->setValue('=-C4:E4'); + self::assertSame('#VALUE!', $sheet->getCell('G4')->getCalculatedValue()); + $sheet->getCell('H4')->setValue('=-A4:B4'); + self::assertSame(3, $sheet->getCell('H4')->getCalculatedValue()); + $sheet->getCell('I4')->setValue('=25%'); + self::assertEqualsWithDelta(0.25, $sheet->getCell('I4')->getCalculatedValue(), 1.0E-8); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php index 780b962374..81dbafedc2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -79,4 +79,27 @@ public function providerSUMWITHINDEXMATCH(): array { return require 'tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php'; } + + public function testSumWithIndexMatchMoved(): void + { + $spreadsheet = new Spreadsheet(); + $sheet1 = $spreadsheet->getActiveSheet(); + $sheet1->setTitle('Formula'); + $sheet1->getCell('G5')->setValue('Number'); + $sheet1->getCell('H5')->setValue('Formula'); + $sheet1->getCell('G6')->setValue(83); + $sheet1->getCell('H6')->setValue('=SUM(4 * INDEX(Lookup!D4:D6, MATCH(G6, Lookup!C4:C6, 0)))'); + $sheet2 = $spreadsheet->createSheet(); + $sheet2->setTitle('Lookup'); + $sheet2->getCell('C3')->setValue('Lookup'); + $sheet2->getCell('D3')->setValue('Match'); + $sheet2->getCell('C4')->setValue(82); + $sheet2->getCell('D4')->setValue(15); + $sheet2->getCell('C5')->setValue(83); + $sheet2->getCell('D5')->setValue(16); + $sheet2->getCell('C6')->setValue(84); + $sheet2->getCell('D6')->setValue(17); + self::assertSame(64, $sheet1->getCell('H6')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxATest.php index 4d3d9ab236..89ec857af2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxATest.php @@ -11,7 +11,7 @@ class MaxATest extends AllSetupTeardown */ public function testMAXA($expectedResult, ...$args): void { - $this->runTestCases('MAXA', $expectedResult, ...$args); + $this->runTestCaseReference('MAXA', $expectedResult, ...$args); } public function providerMAXA(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxTest.php index 2cd79186f2..9818750d84 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MaxTest.php @@ -11,7 +11,7 @@ class MaxTest extends AllSetupTeardown */ public function testMAX($expectedResult, ...$args): void { - $this->runTestCases('MAX', $expectedResult, ...$args); + $this->runTestCaseReference('MAX', $expectedResult, ...$args); } public function providerMAX(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinATest.php index 1051521dcf..87e85b1302 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinATest.php @@ -11,7 +11,7 @@ class MinATest extends AllSetupTeardown */ public function testMINA($expectedResult, ...$args): void { - $this->runTestCases('MINA', $expectedResult, ...$args); + $this->runTestCaseReference('MINA', $expectedResult, ...$args); } public function providerMINA(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinTest.php index e61f79ba3e..bb4dc24f0d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/MinTest.php @@ -11,7 +11,7 @@ class MinTest extends AllSetupTeardown */ public function testMIN($expectedResult, ...$args): void { - $this->runTestCases('MIN', $expectedResult, ...$args); + $this->runTestCaseReference('MIN', $expectedResult, ...$args); } public function providerMIN(): array diff --git a/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php b/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php index b1152f3f0a..09f3211884 100644 --- a/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/StringLengthTest.php @@ -29,6 +29,17 @@ public function testStringLength(): void self::assertSame('ABCDEF' . str_repeat('Ԁ', DataType::MAX_STRING_LENGTH - 6), $sheet->getCell('C7')->getCalculatedValue(), 'truncate literal concat with cell'); $sheet->getCell('C8')->setValue('="ABCDE" & C1'); self::assertSame('ABCDE' . $longstring, $sheet->getCell('C8')->getCalculatedValue(), 'okay literal concat with cell'); + $sheet->getCell('C9')->setValue('=false & true & 3'); + self::assertSame('FALSETRUE3', $sheet->getCell('C9')->getCalculatedValue()); + $sheet->getCell('D8')->setValue('abcde'); + $sheet->getCell('D9')->setValue('=D8 & "*" & D8'); + self::assertSame('abcde*abcde', $sheet->getCell('D9')->getCalculatedValue()); + $sheet->getCell('E8')->setValue('"abcde"'); + $sheet->getCell('E9')->setValue('=E8 & "*" & E8'); + self::assertSame('"abcde"*"abcde"', $sheet->getCell('E9')->getCalculatedValue()); + $sheet->getCell('F8')->setValue('"abcde"'); + $sheet->getCell('F9')->setValue('=F8 & "*" & "abcde"'); + self::assertSame('"abcde"*abcde', $sheet->getCell('F9')->getCalculatedValue()); $spreadsheet->disconnectWorksheets(); } } diff --git a/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php b/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php index b62cfd30d5..35555830ee 100644 --- a/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php +++ b/tests/data/Calculation/MathTrig/SUMWITHINDEXMATCH.php @@ -25,6 +25,12 @@ [ 4, '=SUM(INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)) / 4)', ], + 'divide by zero' => [ + '#DIV/0!', '=SUM(INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)) / 0)', + ], + 'invalid divisor' => [ + '#VALUE!', '=SUM(INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)) / "xyz")', + ], [ 4294967296, '=SUM(4 ^ INDEX(Lookup!B2, MATCH(A2, Lookup!A2, 0)))', ], diff --git a/tests/data/Calculation/Statistical/MAX.php b/tests/data/Calculation/Statistical/MAX.php index 635853070d..fc1f8a0e1a 100644 --- a/tests/data/Calculation/Statistical/MAX.php +++ b/tests/data/Calculation/Statistical/MAX.php @@ -13,4 +13,8 @@ 0, null, 'STRING', true, '', '27', ], + 'error among arguments' => [ + '#DIV/0!', + 1, 3, '=5/0', -2, + ], ]; diff --git a/tests/data/Calculation/Statistical/MAXA.php b/tests/data/Calculation/Statistical/MAXA.php index 30b3928aa9..992140e79b 100644 --- a/tests/data/Calculation/Statistical/MAXA.php +++ b/tests/data/Calculation/Statistical/MAXA.php @@ -29,4 +29,8 @@ 0, null, null, null, null, ], + 'error among arguments' => [ + '#DIV/0!', + 1, 3, '=5/0', -2, + ], ]; diff --git a/tests/data/Calculation/Statistical/MIN.php b/tests/data/Calculation/Statistical/MIN.php index f5c350bd9b..f53fc6e5b9 100644 --- a/tests/data/Calculation/Statistical/MIN.php +++ b/tests/data/Calculation/Statistical/MIN.php @@ -13,4 +13,8 @@ 0, null, 'STRING', true, '', '27', ], + 'error among arguments' => [ + '#DIV/0!', + 1, 3, '=5/0', -2, + ], ]; diff --git a/tests/data/Calculation/Statistical/MINA.php b/tests/data/Calculation/Statistical/MINA.php index 5f1fbd2ab7..5722eb5622 100644 --- a/tests/data/Calculation/Statistical/MINA.php +++ b/tests/data/Calculation/Statistical/MINA.php @@ -29,4 +29,8 @@ 0, null, null, null, null, ], + 'error among arguments' => [ + '#DIV/0!', + 1, 3, '=5/0', -2, + ], ];