From 3764f303547eb5e941738dbe362b7a6f615835c7 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 22 Feb 2021 12:46:57 +0100 Subject: [PATCH 01/89] Refactor the Excel Database functions; and rewrite the query building (#1871) * Refactor the Excel Database functions; and rewrite the query building to fix a bug with complex multi-criteria queries that involve both AND and OR conditions * Fix handling for empty cells and NULL values in searches * Expand unit tests; and add TODOs for dates, percentages, and wildcard text comparisons --- .../Calculation/Calculation.php | 25 +- src/PhpSpreadsheet/Calculation/Database.php | 308 ++++-------------- .../Calculation/Database/DAverage.php | 45 +++ .../Calculation/Database/DCount.php | 43 +++ .../Calculation/Database/DCountA.php | 42 +++ .../Calculation/Database/DGet.php | 49 +++ .../Calculation/Database/DMax.php | 46 +++ .../Calculation/Database/DMin.php | 46 +++ .../Calculation/Database/DProduct.php | 45 +++ .../Calculation/Database/DStDev.php | 46 +++ .../Calculation/Database/DStDevP.php | 46 +++ .../Calculation/Database/DSum.php | 45 +++ .../Calculation/Database/DVar.php | 46 +++ .../Calculation/Database/DVarP.php | 46 +++ .../Calculation/Database/DatabaseAbstract.php | 144 ++++++++ .../Functions/Database/DAverageTest.php | 110 +++++++ .../Functions/Database/DCountATest.php | 108 ++++++ .../Functions/Database/DCountTest.php | 103 ++++++ .../Functions/Database/DGetTest.php | 115 +++++++ .../Functions/Database/DMaxTest.php | 105 ++++++ .../Functions/Database/DMinTest.php | 101 ++++++ .../Functions/Database/DProductTest.php | 105 ++++++ .../Functions/Database/DStDevPTest.php | 101 ++++++ .../Functions/Database/DStDevTest.php | 101 ++++++ .../Functions/Database/DSumTest.php | 117 +++++++ .../Functions/Database/DVarPTest.php | 101 ++++++ .../Functions/Database/DVarTest.php | 101 ++++++ 27 files changed, 2034 insertions(+), 256 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Database/DAverage.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DCount.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DCountA.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DGet.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DMax.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DMin.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DProduct.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DStDev.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DStDevP.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DSum.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DVar.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DVarP.php create mode 100644 src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DAverageTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DGetTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMaxTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMinTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevPTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarPTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarTest.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 7868ec5e..87bf44a5 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -769,7 +769,7 @@ class Calculation ], 'DAVERAGE' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DAVERAGE'], + 'functionCall' => [Database\DAverage::class, 'evaluate'], 'argumentCount' => '3', ], 'DAY' => [ @@ -799,12 +799,12 @@ class Calculation ], 'DCOUNT' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DCOUNT'], + 'functionCall' => [Database\DCount::class, 'evaluate'], 'argumentCount' => '3', ], 'DCOUNTA' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DCOUNTA'], + 'functionCall' => [Database\DCountA::class, 'evaluate'], 'argumentCount' => '3', ], 'DDB' => [ @@ -849,7 +849,7 @@ class Calculation ], 'DGET' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DGET'], + 'functionCall' => [Database\DGet::class, 'evaluate'], 'argumentCount' => '3', ], 'DISC' => [ @@ -859,12 +859,12 @@ class Calculation ], 'DMAX' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DMAX'], + 'functionCall' => [Database\DMax::class, 'evaluate'], 'argumentCount' => '3', ], 'DMIN' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DMIN'], + 'functionCall' => [Database\DMin::class, 'evaluate'], 'argumentCount' => '3', ], 'DOLLAR' => [ @@ -884,22 +884,22 @@ class Calculation ], 'DPRODUCT' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DPRODUCT'], + 'functionCall' => [Database\DProduct::class, 'evaluate'], 'argumentCount' => '3', ], 'DSTDEV' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DSTDEV'], + 'functionCall' => [Database\DStDev::class, 'evaluate'], 'argumentCount' => '3', ], 'DSTDEVP' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DSTDEVP'], + 'functionCall' => [Database\DStDevP::class, 'evaluate'], 'argumentCount' => '3', ], 'DSUM' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DSUM'], + 'functionCall' => [Database\DSum::class, 'evaluate'], 'argumentCount' => '3', ], 'DURATION' => [ @@ -909,12 +909,12 @@ class Calculation ], 'DVAR' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DVAR'], + 'functionCall' => [Database\DVar::class, 'evaluate'], 'argumentCount' => '3', ], 'DVARP' => [ 'category' => Category::CATEGORY_DATABASE, - 'functionCall' => [Database::class, 'DVARP'], + 'functionCall' => [Database\DVarP::class, 'evaluate'], 'argumentCount' => '3', ], 'EDATE' => [ @@ -3437,6 +3437,7 @@ class Calculation $this->debugLog->writeDebugLog('Formula for cell ', $wsCellReference, ' is ', $formula); // Parse the formula onto the token stack and calculate the value $this->cyclicReferenceStack->push($wsCellReference); + $cellValue = $this->processTokenStack($this->internalParseFormula($formula, $pCell), $cellID, $pCell); $this->cyclicReferenceStack->pop(); diff --git a/src/PhpSpreadsheet/Calculation/Database.php b/src/PhpSpreadsheet/Calculation/Database.php index 5250e306..db16e2df 100644 --- a/src/PhpSpreadsheet/Calculation/Database.php +++ b/src/PhpSpreadsheet/Calculation/Database.php @@ -2,126 +2,11 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; +/** + * @deprecated 1.17.0 + */ class Database { - /** - * fieldExtract. - * - * Extracts the column ID to use for the data field. - * - * @param mixed[] $database The range of cells that makes up the list or database. - * A database is a list of related data in which rows of related - * information are records, and columns of data are fields. The - * first row of the list contains labels for each column. - * @param mixed $field Indicates which column is used in the function. Enter the - * column label enclosed between double quotation marks, such as - * "Age" or "Yield," or a number (without quotation marks) that - * represents the position of the column within the list: 1 for - * the first column, 2 for the second column, and so on. - * - * @return null|string - */ - private static function fieldExtract($database, $field) - { - $field = strtoupper(Functions::flattenSingleValue($field)); - $fieldNames = array_map('strtoupper', array_shift($database)); - - if (is_numeric($field)) { - $keys = array_keys($fieldNames); - - return $keys[$field - 1]; - } - $key = array_search($field, $fieldNames); - - return $key ?: null; - } - - /** - * filter. - * - * Parses the selection criteria, extracts the database rows that match those criteria, and - * returns that subset of rows. - * - * @param mixed[] $database The range of cells that makes up the list or database. - * A database is a list of related data in which rows of related - * information are records, and columns of data are fields. The - * first row of the list contains labels for each column. - * @param mixed[] $criteria The range of cells that contains the conditions you specify. - * You can use any range for the criteria argument, as long as it - * includes at least one column label and at least one cell below - * the column label in which you specify a condition for the - * column. - * - * @return array of mixed - */ - private static function filter($database, $criteria) - { - $fieldNames = array_shift($database); - $criteriaNames = array_shift($criteria); - - // Convert the criteria into a set of AND/OR conditions with [:placeholders] - $testConditions = $testValues = []; - $testConditionsCount = 0; - foreach ($criteriaNames as $key => $criteriaName) { - $testCondition = []; - $testConditionCount = 0; - foreach ($criteria as $row => $criterion) { - if ($criterion[$key] > '') { - $testCondition[] = '[:' . $criteriaName . ']' . Functions::ifCondition($criterion[$key]); - ++$testConditionCount; - } - } - if ($testConditionCount > 1) { - $testConditions[] = 'OR(' . implode(',', $testCondition) . ')'; - ++$testConditionsCount; - } elseif ($testConditionCount == 1) { - $testConditions[] = $testCondition[0]; - ++$testConditionsCount; - } - } - - if ($testConditionsCount > 1) { - $testConditionSet = 'AND(' . implode(',', $testConditions) . ')'; - } elseif ($testConditionsCount == 1) { - $testConditionSet = $testConditions[0]; - } - - // Loop through each row of the database - foreach ($database as $dataRow => $dataValues) { - // Substitute actual values from the database row for our [:placeholders] - $testConditionList = $testConditionSet; - foreach ($criteriaNames as $key => $criteriaName) { - $k = array_search($criteriaName, $fieldNames); - if (isset($dataValues[$k])) { - $dataValue = $dataValues[$k]; - $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; - $testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList); - } - } - // evaluate the criteria against the row data - $result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList); - // If the row failed to meet the criteria, remove it from the database - if (!$result) { - unset($database[$dataRow]); - } - } - - return $database; - } - - private static function getFilteredColumn($database, $field, $criteria) - { - // reduce the database to a set of rows that match all the criteria - $database = self::filter($database, $criteria); - // extract an array of values for the requested column - $colData = []; - foreach ($database as $row) { - $colData[] = $row[$field]; - } - - return $colData; - } - /** * DAVERAGE. * @@ -130,6 +15,10 @@ class Database * Excel Function: * DAVERAGE(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DAverage class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -149,15 +38,7 @@ class Database */ public static function DAVERAGE($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::AVERAGE( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DAverage::evaluate($database, $field, $criteria); } /** @@ -169,14 +50,15 @@ class Database * Excel Function: * DCOUNT(database,[field],criteria) * - * Excel Function: - * DAVERAGE(database,field,criteria) + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DCount class instead * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The * first row of the list contains labels for each column. - * @param int|string $field Indicates which column is used in the function. Enter the + * @param null|int|string $field Indicates which column is used in the function. Enter the * column label enclosed between double quotation marks, such as * "Age" or "Yield," or a number (without quotation marks) that * represents the position of the column within the list: 1 for @@ -194,15 +76,7 @@ class Database */ public static function DCOUNT($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::COUNT( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DCount::evaluate($database, $field, $criteria); } /** @@ -213,11 +87,15 @@ class Database * Excel Function: * DCOUNTA(database,[field],criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DCountA class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The * first row of the list contains labels for each column. - * @param int|string $field Indicates which column is used in the function. Enter the + * @param null|int|string $field Indicates which column is used in the function. Enter the * column label enclosed between double quotation marks, such as * "Age" or "Yield," or a number (without quotation marks) that * represents the position of the column within the list: 1 for @@ -229,29 +107,10 @@ class Database * column. * * @return int - * - * @TODO The field argument is optional. If field is omitted, DCOUNTA counts all records in the - * database that match the criteria. */ public static function DCOUNTA($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // reduce the database to a set of rows that match all the criteria - $database = self::filter($database, $criteria); - // extract an array of values for the requested column - $colData = []; - foreach ($database as $row) { - $colData[] = $row[$field]; - } - - // Return - return Statistical::COUNTA( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DCountA::evaluate($database, $field, $criteria); } /** @@ -263,6 +122,10 @@ class Database * Excel Function: * DGET(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DGet class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -282,18 +145,7 @@ class Database */ public static function DGET($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - $colData = self::getFilteredColumn($database, $field, $criteria); - if (count($colData) > 1) { - return Functions::NAN(); - } - - return $colData[0]; + return Database\DGet::evaluate($database, $field, $criteria); } /** @@ -305,6 +157,10 @@ class Database * Excel Function: * DMAX(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DMax class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -324,15 +180,7 @@ class Database */ public static function DMAX($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::MAX( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DMax::evaluate($database, $field, $criteria); } /** @@ -344,6 +192,10 @@ class Database * Excel Function: * DMIN(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DMin class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -363,15 +215,7 @@ class Database */ public static function DMIN($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::MIN( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DMin::evaluate($database, $field, $criteria); } /** @@ -382,6 +226,10 @@ class Database * Excel Function: * DPRODUCT(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DProduct class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -401,15 +249,7 @@ class Database */ public static function DPRODUCT($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return MathTrig::PRODUCT( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DProduct::evaluate($database, $field, $criteria); } /** @@ -421,6 +261,10 @@ class Database * Excel Function: * DSTDEV(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DStDev class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -440,15 +284,7 @@ class Database */ public static function DSTDEV($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::STDEV( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DStDev::evaluate($database, $field, $criteria); } /** @@ -460,6 +296,10 @@ class Database * Excel Function: * DSTDEVP(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DStDevP class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -479,15 +319,7 @@ class Database */ public static function DSTDEVP($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::STDEVP( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DStDevP::evaluate($database, $field, $criteria); } /** @@ -498,6 +330,10 @@ class Database * Excel Function: * DSUM(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DSum class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -517,15 +353,7 @@ class Database */ public static function DSUM($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return MathTrig::SUM( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DSum::evaluate($database, $field, $criteria); } /** @@ -537,6 +365,10 @@ class Database * Excel Function: * DVAR(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DVar class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -556,15 +388,7 @@ class Database */ public static function DVAR($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::VARFunc( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DVar::evaluate($database, $field, $criteria); } /** @@ -576,6 +400,10 @@ class Database * Excel Function: * DVARP(database,field,criteria) * + * @Deprecated 1.17.0 + * + * @see Use the evaluate() method in the Database\DVarP class instead + * * @param mixed[] $database The range of cells that makes up the list or database. * A database is a list of related data in which rows of related * information are records, and columns of data are fields. The @@ -595,14 +423,6 @@ class Database */ public static function DVARP($database, $field, $criteria) { - $field = self::fieldExtract($database, $field); - if ($field === null) { - return null; - } - - // Return - return Statistical::VARP( - self::getFilteredColumn($database, $field, $criteria) - ); + return Database\DVarP::evaluate($database, $field, $criteria); } } diff --git a/src/PhpSpreadsheet/Calculation/Database/DAverage.php b/src/PhpSpreadsheet/Calculation/Database/DAverage.php new file mode 100644 index 00000000..899b2426 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Database/DAverage.php @@ -0,0 +1,45 @@ + 1) { + return Functions::NAN(); + } + + return $columnData[0]; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Database/DMax.php b/src/PhpSpreadsheet/Calculation/Database/DMax.php new file mode 100644 index 00000000..8918c54d --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Database/DMax.php @@ -0,0 +1,46 @@ +, <=, etc) + * @TODO Suport for formatted numerics (e.g. '>12.5%' => '>0.125') + * @TODO Suport for wildcard ? and * in strings (includng escaping) + */ + private static function buildQuery(array $criteriaNames, array $criteria): string + { + $baseQuery = []; + foreach ($criteria as $key => $criterion) { + foreach ($criterion as $field => $value) { + $criterionName = $criteriaNames[$field]; + if ($value !== null && $value !== '') { + $condition = '[:' . $criterionName . ']' . Functions::ifCondition($value); + $baseQuery[$key][] = $condition; + } + } + } + + $rowQuery = array_map( + function ($rowValue) { + return (count($rowValue) > 1) ? 'AND(' . implode(',', $rowValue) . ')' : $rowValue[0]; + }, + $baseQuery + ); + + return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : $rowQuery[0]; + } + + /** + * @param $criteriaNames + * @param $fieldNames + */ + private static function executeQuery(array $database, string $query, $criteriaNames, $fieldNames): array + { + foreach ($database as $dataRow => $dataValues) { + // Substitute actual values from the database row for our [:placeholders] + $testConditionList = $query; + foreach ($criteriaNames as $key => $criteriaName) { + $key = array_search($criteriaName, $fieldNames, true); + if (isset($dataValues[$key])) { + $dataValue = $dataValues[$key]; + $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; + } else { + $dataValue = 'NULL'; + } + $testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList); + } + + // evaluate the criteria against the row data + $result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList); + // If the row failed to meet the criteria, remove it from the database + + if ($result !== true) { + unset($database[$dataRow]); + } + } + + return $database; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DAverageTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DAverageTest.php new file mode 100644 index 00000000..91011504 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DAverageTest.php @@ -0,0 +1,110 @@ +database1(), + 'Yield', + [ + ['Tree', 'Height'], + ['=Apple', '>10'], + ], + ], + [ + 13, + $this->database1(), + 3, + $this->database1(), + ], + [ + 268333.333333333333, + $this->database2(), + 'Sales', + [ + ['Quarter', 'Sales Rep.'], + ['>1', 'Tina'], + ], + ], + [ + 372500, + $this->database2(), + 'Sales', + [ + ['Quarter', 'Area'], + ['1', 'South'], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php new file mode 100644 index 00000000..ea856c57 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php @@ -0,0 +1,108 @@ +database1(), + 'Profit', + [ + ['Tree', 'Height', 'Height'], + ['=Apple', '>10', '<16'], + ], + ], + [ + 2, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['Science', 'Male'], + ], + ], + /* + * Null value in datacolumn behaviour for DCOUNTA... will include not include a null value in the count + * if it is an actual cell value; but it will be included if it is a literal... this test case is + * currently passing literals + [ + 1, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['Math', 'Female'], + ], + ], + */ + [ + 3, + $this->database2(), + 'Score', + [ + ['Subject', 'Score'], + ['English', '>0.60'], + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php new file mode 100644 index 00000000..9e12ac96 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php @@ -0,0 +1,103 @@ +database1(), + 'Age', + [ + ['Tree', 'Height', 'Height'], + ['=Apple', '>10', '<16'], + ], + ], + [ + 1, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['Science', 'Male'], + ], + ], + [ + 1, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['Math', 'Female'], + ], + ], + [ + 3, + $this->database2(), + null, + [ + ['Subject', 'Score'], + ['English', '>0.63'], + ], + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DGetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DGetTest.php new file mode 100644 index 00000000..3d90ff96 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DGetTest.php @@ -0,0 +1,115 @@ +database1(), + 'Yield', + [ + ['Tree'], + ['=Apple'], + ['=Pear'], + ], + ], + [ + 10, + $this->database1(), + 'Yield', + [ + ['Tree', 'Height', 'Height'], + ['=Apple', '>10', '<16'], + ['=Pear', '>12', null], + ], + ], + [ + 188000, + $this->database2(), + 'Sales', + [ + ['Sales Rep.', 'Quarter'], + ['Tina', 4], + ], + ], + [ + Functions::NAN(), + $this->database2(), + 'Sales', + [ + ['Area', 'Quarter'], + ['South', 4], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMaxTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMaxTest.php new file mode 100644 index 00000000..b0ad59a7 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMaxTest.php @@ -0,0 +1,105 @@ +database1(), + 'Profit', + [ + ['Tree', 'Height', 'Height'], + ['=Apple', '>10', '<16'], + ['=Pear', null, null], + ], + ], + [ + 340000, + $this->database2(), + 'Sales', + [ + ['Quarter', 'Area'], + [2, 'North'], + ], + ], + [ + 460000, + $this->database2(), + 'Sales', + [ + ['Sales Rep.', 'Quarter'], + ['Carol', '>1'], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMinTest.php new file mode 100644 index 00000000..bfd2af4c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DMinTest.php @@ -0,0 +1,101 @@ +database1(), + 'Profit', + [ + ['Tree', 'Height', 'Height'], + ['=Apple', '>10', '<16'], + ['=Pear', '>12', null], + ], + ], + [ + 0.48, + $this->database2(), + 'Score', + [ + ['Subject', 'Age'], + ['Science', '>8'], + ], + ], + [ + 0.55, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['Math', 'Male'], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php new file mode 100644 index 00000000..9b2f92b3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php @@ -0,0 +1,105 @@ +database1(), + 'Yield', + [ + ['Tree', 'Height', 'Height'], + ['=Apple', '>10', '<16'], + ['=Pear', null, null], + ], + ], + /* + * We don't yet support date handling in the search query + [ + 36, + $this->database2(), + 'Score', + [ + ['Name', 'Date'], + ['Gary', '05-Jan-2017'], + ], + ], + [ + 8, + $this->database2(), + 'Score', + [ + ['Test', 'Date'], + ['Test1', '<05-Jan-2017'], + ], + ], + */ + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevPTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevPTest.php new file mode 100644 index 00000000..210325d5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevPTest.php @@ -0,0 +1,101 @@ +database1(), + 'Yield', + [ + ['Tree'], + ['=Apple'], + ['=Pear'], + ], + ], + [ + 0.085244745684, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['English', 'Male'], + ], + ], + [ + 0.160623784042, + $this->database2(), + 'Score', + [ + ['Subject', 'Age'], + ['Math', '>8'], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevTest.php new file mode 100644 index 00000000..71bbcd2a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DStDevTest.php @@ -0,0 +1,101 @@ +database1(), + 'Yield', + [ + ['Tree'], + ['=Apple'], + ['=Pear'], + ], + ], + [ + 0.104403065089, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['English', 'Male'], + ], + ], + [ + 0.196723155729, + $this->database2(), + 'Score', + [ + ['Subject', 'Age'], + ['Math', '>8'], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php new file mode 100644 index 00000000..fbb86bb9 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php @@ -0,0 +1,117 @@ +database1(), + 'Profit', + [ + ['Tree'], + ['=Apple'], + ], + ], + [ + 248, + $this->database1(), + 'Profit', + [ + ['Tree', 'Height', 'Height'], + ['=Apple', '>10', '<16'], + ['=Pear', null, null], + ], + ], + [ + 1210000, + $this->database2(), + 'Sales', + [ + ['Quarter', 'Area'], + ['>2', 'North'], + ], + ], + /* + * We don't yet support woldcards in text search fields + [ + 710000, + $this->database2(), + 'Sales', + [ + ['Quarter', 'Sales Rep.'], + ['3', 'C*'], + ], + ], + */ + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarPTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarPTest.php new file mode 100644 index 00000000..0436ea67 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarPTest.php @@ -0,0 +1,101 @@ +database1(), + 'Yield', + [ + ['Tree'], + ['=Apple'], + ['=Pear'], + ], + ], + [ + 0.025622222222, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['Math', 'Male'], + ], + ], + [ + 0.011622222222, + $this->database2(), + 'Score', + [ + ['Subject', 'Age'], + ['Science', '>8'], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarTest.php new file mode 100644 index 00000000..467db2f2 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DVarTest.php @@ -0,0 +1,101 @@ +database1(), + 'Yield', + [ + ['Tree'], + ['=Apple'], + ['=Pear'], + ], + ], + [ + 0.038433333333, + $this->database2(), + 'Score', + [ + ['Subject', 'Gender'], + ['Math', 'Male'], + ], + ], + [ + 0.017433333333, + $this->database2(), + 'Score', + [ + ['Subject', 'Age'], + ['Science', '>8'], + ], + ], + [ + null, + $this->database1(), + null, + $this->database1(), + ], + ]; + } +} From 40a6dee0a42fb162c57d61bd729dff504249f337 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 22 Feb 2021 20:40:40 +0100 Subject: [PATCH 02/89] Enable support for dates and percentages in Excel Database functions (#1875) * Enable support for dates and percentages in Excel Database functions, and CountIf/AverageIf/etc * Enable support for booleans in Excel Database functions --- CHANGELOG.md | 1 + .../Calculation/Database/DatabaseAbstract.php | 7 ++-- src/PhpSpreadsheet/Calculation/Functions.php | 31 ++++++++++++++-- .../Functions/Database/DCountATest.php | 2 +- .../Functions/Database/DCountTest.php | 35 ++++++++++++++++++- .../Functions/Database/DProductTest.php | 3 -- .../Functions/Database/DSumTest.php | 2 +- 7 files changed, 68 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bdeaaa8e..6c649844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Support for date values and percentages in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1875](https://github.com/PHPOffice/PhpSpreadsheet/pull/1875) - Implemented DataBar for conditional formatting in Xlsx, providing read/write and creation of (type, value, direction, fills, border, axis position, color settings) as DataBar options in Excel. [#1754](https://github.com/PHPOffice/PhpSpreadsheet/pull/1754) - Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796) - Basic implementation of the PERMUTATIONA() Statistical Function diff --git a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php index 22ce0957..ae2c3fd7 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php +++ b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -83,8 +83,6 @@ abstract class DatabaseAbstract } /** - * @TODO Support for Dates (including handling for >, <=, etc) - * @TODO Suport for formatted numerics (e.g. '>12.5%' => '>0.125') * @TODO Suport for wildcard ? and * in strings (includng escaping) */ private static function buildQuery(array $criteriaNames, array $criteria): string @@ -121,7 +119,9 @@ abstract class DatabaseAbstract $testConditionList = $query; foreach ($criteriaNames as $key => $criteriaName) { $key = array_search($criteriaName, $fieldNames, true); - if (isset($dataValues[$key])) { + if (is_bool($dataValues[$key])) { + $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; + } elseif ($dataValues[$key] !== null) { $dataValue = $dataValues[$key]; $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; } else { @@ -129,7 +129,6 @@ abstract class DatabaseAbstract } $testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList); } - // evaluate the criteria against the row data $result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList); // If the row failed to meet the criteria, remove it from the database diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 2e8a7ecf..022e6be5 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use PhpOffice\PhpSpreadsheet\Cell\Cell; +use PhpOffice\PhpSpreadsheet\Shared\Date; class Functions { @@ -252,9 +253,11 @@ class Functions if ($condition === '') { $condition = '=""'; } - if (!is_string($condition) || !in_array($condition[0], ['>', '<', '='])) { - if (!is_numeric($condition)) { + $condition = self::operandSpecialHandling($condition); + if (is_bool($condition)) { + return '=' . ($condition ? 'TRUE' : 'FALSE'); + } elseif (!is_numeric($condition)) { $condition = Calculation::wrapResult(strtoupper($condition)); } @@ -263,9 +266,10 @@ class Functions preg_match('/(=|<[>=]?|>=?)(.*)/', $condition, $matches); [, $operator, $operand] = $matches; + $operand = self::operandSpecialHandling($operand); if (is_numeric(trim($operand, '"'))) { $operand = trim($operand, '"'); - } elseif (!is_numeric($operand)) { + } elseif (!is_numeric($operand) && $operand !== 'FALSE' && $operand !== 'TRUE') { $operand = str_replace('"', '""', $operand); $operand = Calculation::wrapResult(strtoupper($operand)); } @@ -273,6 +277,27 @@ class Functions return str_replace('""""', '""', $operator . $operand); } + private static function operandSpecialHandling($operand) + { + if (is_numeric($operand) || is_bool($operand)) { + return $operand; + } elseif (strtoupper($operand) === Calculation::getTRUE() || strtoupper($operand) === Calculation::getFALSE()) { + return strtoupper($operand); + } + + // Check for percentage + if (preg_match('/^\-?\d*\.?\d*\s?\%$/', $operand)) { + return ((float) rtrim($operand, '%')) / 100; + } + + // Check for dates + if (($dateValueOperand = Date::stringToExcel($operand)) !== false) { + return $dateValueOperand; + } + + return $operand; + } + /** * ERROR_TYPE. * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php index ea856c57..4cfea42f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php @@ -100,7 +100,7 @@ class DCountATest extends TestCase 'Score', [ ['Subject', 'Score'], - ['English', '>0.60'], + ['English', '>60%'], ], ], ]; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php index 9e12ac96..11d8adab 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountTest.php @@ -59,6 +59,21 @@ class DCountTest extends TestCase ]; } + protected function database3() + { + return [ + ['Status', 'Value'], + [false, 1], + [true, 2], + [true, 4], + [false, 8], + [true, 16], + [false, 32], + [false, 64], + [false, 128], + ]; + } + public function providerDCount() { return [ @@ -95,7 +110,25 @@ class DCountTest extends TestCase null, [ ['Subject', 'Score'], - ['English', '>0.63'], + ['English', '>63%'], + ], + ], + [ + 3, + $this->database3(), + 'Value', + [ + ['Status'], + [true], + ], + ], + [ + 5, + $this->database3(), + 'Value', + [ + ['Status'], + ['<>true'], ], ], ]; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php index 9b2f92b3..0fb1bba7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DProductTest.php @@ -73,8 +73,6 @@ class DProductTest extends TestCase ['=Pear', null, null], ], ], - /* - * We don't yet support date handling in the search query [ 36, $this->database2(), @@ -93,7 +91,6 @@ class DProductTest extends TestCase ['Test1', '<05-Jan-2017'], ], ], - */ [ null, $this->database1(), diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php index fbb86bb9..2a197cd1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php @@ -95,7 +95,7 @@ class DSumTest extends TestCase ], ], /* - * We don't yet support woldcards in text search fields + * We don't yet support wildcards in text search fields [ 710000, $this->database2(), From 25f7dcb9fd8fe437bbe524d55e78a65ac61094c8 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 23 Feb 2021 19:26:29 +0100 Subject: [PATCH 03/89] Enable support for wildcard text searches in Excel Database functions (#1876) * Enable support for wildcard text searches in Excel Database functions --- CHANGELOG.md | 1 + .../Calculation/Calculation.php | 11 ++- .../Calculation/Database/DatabaseAbstract.php | 69 ++++++++++++------- .../Calculation/Internal/MakeMatrix.php | 11 +++ .../Calculation/Internal/WildcardMatch.php | 34 +++++++++ .../Functions/Database/DSumTest.php | 3 - 6 files changed, 96 insertions(+), 33 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php create mode 100644 src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c649844..f8baacc0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added - Support for date values and percentages in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1875](https://github.com/PHPOffice/PhpSpreadsheet/pull/1875) +- Support for booleans, and for wildcard text search in query parameters for Database functions. [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876) - Implemented DataBar for conditional formatting in Xlsx, providing read/write and creation of (type, value, direction, fills, border, axis position, color settings) as DataBar options in Excel. [#1754](https://github.com/PHPOffice/PhpSpreadsheet/pull/1754) - Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796) - Basic implementation of the PERMUTATIONA() Statistical Function diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 87bf44a5..ba629957 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2663,12 +2663,16 @@ class Calculation private static $controlFunctions = [ 'MKMATRIX' => [ 'argumentCount' => '*', - 'functionCall' => [__CLASS__, 'mkMatrix'], + 'functionCall' => [Internal\MakeMatrix::class, 'make'], ], 'NAME.ERROR' => [ 'argumentCount' => '*', 'functionCall' => [Functions::class, 'NAME'], ], + 'WILDCARDMATCH' => [ + 'argumentCount' => '2', + 'functionCall' => [Internal\WildcardMatch::class, 'compare'], + ], ]; public function __construct(?Spreadsheet $spreadsheet = null) @@ -3742,11 +3746,6 @@ class Calculation return $formula; } - private static function mkMatrix(...$args) - { - return $args; - } - // Binary Operators // These operators always work on two values // Array key is the operator, the value indicates whether this is a left or right associative operator diff --git a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php index ae2c3fd7..a08f1251 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php +++ b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch; abstract class DatabaseAbstract { @@ -82,9 +83,6 @@ abstract class DatabaseAbstract return $columnData; } - /** - * @TODO Suport for wildcard ? and * in strings (includng escaping) - */ private static function buildQuery(array $criteriaNames, array $criteria): string { $baseQuery = []; @@ -92,7 +90,7 @@ abstract class DatabaseAbstract foreach ($criterion as $field => $value) { $criterionName = $criteriaNames[$field]; if ($value !== null && $value !== '') { - $condition = '[:' . $criterionName . ']' . Functions::ifCondition($value); + $condition = self::buildCondition($value, $criterionName); $baseQuery[$key][] = $condition; } } @@ -108,31 +106,39 @@ abstract class DatabaseAbstract return (count($rowQuery) > 1) ? 'OR(' . implode(',', $rowQuery) . ')' : $rowQuery[0]; } - /** - * @param $criteriaNames - * @param $fieldNames - */ - private static function executeQuery(array $database, string $query, $criteriaNames, $fieldNames): array + private static function buildCondition($criterion, string $criterionName): string + { + $ifCondition = Functions::ifCondition($criterion); + + // Check for wildcard characters used in the condition + $result = preg_match('/(?[^"]*)(?".*[*?].*")/ui', $ifCondition, $matches); + if ($result !== 1) { + return "[:{$criterionName}]{$ifCondition}"; + } + + $trueFalse = ($matches['operator'] !== '<>'); + $wildcard = WildcardMatch::wildcard($matches['operand']); + $condition = "WILDCARDMATCH([:{$criterionName}],{$wildcard})"; + if ($trueFalse === false) { + $condition = "NOT({$condition})"; + } + + return $condition; + } + + private static function executeQuery(array $database, string $query, array $criteria, array $fields): array { foreach ($database as $dataRow => $dataValues) { // Substitute actual values from the database row for our [:placeholders] - $testConditionList = $query; - foreach ($criteriaNames as $key => $criteriaName) { - $key = array_search($criteriaName, $fieldNames, true); - if (is_bool($dataValues[$key])) { - $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; - } elseif ($dataValues[$key] !== null) { - $dataValue = $dataValues[$key]; - $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; - } else { - $dataValue = 'NULL'; - } - $testConditionList = str_replace('[:' . $criteriaName . ']', $dataValue, $testConditionList); + $conditions = $query; + foreach ($criteria as $criterion) { + $conditions = self::processCondition($criterion, $fields, $dataValues, $conditions); } - // evaluate the criteria against the row data - $result = Calculation::getInstance()->_calculateFormulaValue('=' . $testConditionList); - // If the row failed to meet the criteria, remove it from the database + // evaluate the criteria against the row data + $result = Calculation::getInstance()->_calculateFormulaValue('=' . $conditions); + + // If the row failed to meet the criteria, remove it from the database if ($result !== true) { unset($database[$dataRow]); } @@ -140,4 +146,19 @@ abstract class DatabaseAbstract return $database; } + + private static function processCondition(string $criterion, array $fields, array $dataValues, string $conditions) + { + $key = array_search($criterion, $fields, true); + + $dataValue = 'NULL'; + if (is_bool($dataValues[$key])) { + $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; + } elseif ($dataValues[$key] !== null) { + $dataValue = $dataValues[$key]; + $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; + } + + return str_replace('[:' . $criterion . ']', $dataValue, $conditions); + } } diff --git a/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php new file mode 100644 index 00000000..8b53464f --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Internal/MakeMatrix.php @@ -0,0 +1,11 @@ +2', 'North'], ], ], - /* - * We don't yet support wildcards in text search fields [ 710000, $this->database2(), @@ -105,7 +103,6 @@ class DSumTest extends TestCase ['3', 'C*'], ], ], - */ [ null, $this->database1(), From cb23cca3ecbc6177dbcd91c4f0a2a141166c722a Mon Sep 17 00:00:00 2001 From: oleibman Date: Sat, 27 Feb 2021 06:10:04 -0800 Subject: [PATCH 04/89] Avoid Duplicate Titles When Reading Multiple HTML Files (#1829) This issue arose while researching issue #1823. The issue was not a bug; it just required clarification to the author of how to use the software. But, while researching, I discovered that loading html into 2 sheets of a spreadsheet has a problem if the html title tag is the same for the 2 sheets. PhpSpreadsheet would be able to save the resulting file, but Excel would not be able to read it properly because of the duplicate title. The worksheet setTitle method allows for disambiguation is such a circumstance. The html reader passed a parameter indicating "don't disambiguate", but I can't see any harm in changing that to "disambiguate". An extremely simple fix, with tests to back it up. --- src/PhpSpreadsheet/Reader/Html.php | 2 +- .../Reader/Html/HtmlLoadStringTest.php | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index e1139015..09148d9f 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -320,7 +320,7 @@ class Html extends BaseReader { if ($child->nodeName === 'title') { $this->processDomElement($child, $sheet, $row, $column, $cellContent); - $sheet->setTitle($cellContent, true, false); + $sheet->setTitle($cellContent, true, true); $cellContent = ''; } else { $this->processDomElementSpanEtc($sheet, $row, $column, $cellContent, $child, $attributeArray); diff --git a/tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php b/tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php index e1041507..bc4c30ff 100644 --- a/tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Html/HtmlLoadStringTest.php @@ -89,4 +89,33 @@ class HtmlLoadStringTest extends TestCase $spreadsheet = $reader->loadFromString($html, $spreadsheet); self::assertEquals(2, $spreadsheet->getSheetCount()); } + + public function testCanLoadDuplicateTitle(): void + { + $html = <<<'EOF' + + +Sheet + + +
1
+ + +EOF; + $reader = new \PhpOffice\PhpSpreadsheet\Reader\Html(); + $spreadsheet = $reader->loadFromString($html); + $reader->setSheetIndex(1); + $reader->loadFromString($html, $spreadsheet); + $reader->setSheetIndex(2); + $reader->loadFromString($html, $spreadsheet); + $sheet = $spreadsheet->getSheet(0); + self::assertEquals(1, $sheet->getCell('A1')->getValue()); + self::assertEquals('Sheet', $sheet->getTitle()); + $sheet = $spreadsheet->getSheet(1); + self::assertEquals(1, $sheet->getCell('A1')->getValue()); + self::assertEquals('Sheet 1', $sheet->getTitle()); + $sheet = $spreadsheet->getSheet(2); + self::assertEquals(1, $sheet->getCell('A1')->getValue()); + self::assertEquals('Sheet 2', $sheet->getTitle()); + } } From 8dcdf581314a6a7ca729699e3e7858b5bbf0ec0f Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Sat, 27 Feb 2021 15:22:25 +0100 Subject: [PATCH 05/89] Update change log --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f8baacc0..6dc53bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Nothing. ### Fixed +- Avoid Duplicate Titles When Reading Multiple HTML Files.[Issue #1823](https://github.com/PHPOffice/PhpSpreadsheet/issues/1823) [PR #1829](https://github.com/PHPOffice/PhpSpreadsheet/pull/1829) - Fixed issue with Worksheet's `getCell()` method when trying to get a cell by defined name. [#1858](https://github.com/PHPOffice/PhpSpreadsheet/issues/1858) - Fix possible endless loop in NumberFormat Masks [#1792](https://github.com/PHPOffice/PhpSpreadsheet/issues/1792) - Fix problem resulting from literal dot inside quotes in number format masks. [PR #1830](https://github.com/PHPOffice/PhpSpreadsheet/pull/1830) From 08673b5820b19dfff9ebb3597578f5e7f7e17422 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 27 Feb 2021 18:26:12 +0100 Subject: [PATCH 06/89] Initial experiments using the new Database query logic with Conditional Statistical Functions (#1880) - Refactoring of the Statistical Conditional functions (`AVERAGEIF()`, `AVERAGEIFS()`, `COUNTIF()`, `COUNTIFS()`, `MAXIFS()` and `MINIFS()` to use the new Database functions codebase. - Extended unit testing - Fix handling for null values - Fixes to wildcard text searches There's still scope for further improvements to memory usage and performance; but for now the code is stable with all unit tests passing --- CHANGELOG.md | 3 +- .../Calculation/Calculation.php | 12 +- src/PhpSpreadsheet/Calculation/Database.php | 2 +- .../Calculation/Database/DAverage.php | 2 +- .../Calculation/Database/DCountA.php | 2 +- .../Calculation/Database/DGet.php | 4 +- .../Calculation/Database/DatabaseAbstract.php | 30 ++- .../Calculation/Internal/WildcardMatch.php | 11 +- src/PhpSpreadsheet/Calculation/MathTrig.php | 4 +- .../Calculation/Statistical.php | 241 +++--------------- .../Calculation/Statistical/Conditional.php | 234 +++++++++++++++++ src/PhpSpreadsheet/Worksheet/Worksheet.php | 28 +- .../Functions/Database/DCountATest.php | 5 - .../Functions/Database/DSumTest.php | 9 + .../Functions/Statistical/AverageIfsTest.php | 31 +++ .../PhpSpreadsheetTests/Helper/SampleTest.php | 3 + .../Calculation/Statistical/AVERAGEIF.php | 35 ++- .../Calculation/Statistical/AVERAGEIFS.php | 42 +++ .../data/Calculation/Statistical/COUNTIF.php | 47 +++- .../data/Calculation/Statistical/COUNTIFS.php | 23 +- tests/data/Calculation/Statistical/MAXIFS.php | 28 ++ tests/data/Calculation/Statistical/MINIFS.php | 28 ++ 22 files changed, 570 insertions(+), 254 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Conditional.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/AverageIfsTest.php create mode 100644 tests/data/Calculation/Statistical/AVERAGEIFS.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dc53bd1..3ab843c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Implementation of the Excel `AVERAGEIFS()` functions as part of a restructuring of Database functions and Conditional Statistical functions. - Support for date values and percentages in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1875](https://github.com/PHPOffice/PhpSpreadsheet/pull/1875) -- Support for booleans, and for wildcard text search in query parameters for Database functions. [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876) +- Support for booleans, and for wildcard text search in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876) - Implemented DataBar for conditional formatting in Xlsx, providing read/write and creation of (type, value, direction, fills, border, axis position, color settings) as DataBar options in Excel. [#1754](https://github.com/PHPOffice/PhpSpreadsheet/pull/1754) - Alignment for ODS Writer [#1796](https://github.com/PHPOffice/PhpSpreadsheet/issues/1796) - Basic implementation of the PERMUTATIONA() Statistical Function diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index ba629957..e114a00d 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -343,12 +343,12 @@ class Calculation ], 'AVERAGEIF' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'AVERAGEIF'], + 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIF'], 'argumentCount' => '2,3', ], 'AVERAGEIFS' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Statistical\Conditional::class, 'AVERAGEIFS'], 'argumentCount' => '3+', ], 'BAHTTEXT' => [ @@ -639,12 +639,12 @@ class Calculation ], 'COUNTIF' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'COUNTIF'], + 'functionCall' => [Statistical\Conditional::class, 'COUNTIF'], 'argumentCount' => '2', ], 'COUNTIFS' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'COUNTIFS'], + 'functionCall' => [Statistical\Conditional::class, 'COUNTIFS'], 'argumentCount' => '2+', ], 'COUPDAYBS' => [ @@ -1630,7 +1630,7 @@ class Calculation ], 'MAXIFS' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MAXIFS'], + 'functionCall' => [Statistical\Conditional::class, 'MAXIFS'], 'argumentCount' => '3+', ], 'MDETERM' => [ @@ -1675,7 +1675,7 @@ class Calculation ], 'MINIFS' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MINIFS'], + 'functionCall' => [Statistical\Conditional::class, 'MINIFS'], 'argumentCount' => '3+', ], 'MINUTE' => [ diff --git a/src/PhpSpreadsheet/Calculation/Database.php b/src/PhpSpreadsheet/Calculation/Database.php index db16e2df..76431979 100644 --- a/src/PhpSpreadsheet/Calculation/Database.php +++ b/src/PhpSpreadsheet/Calculation/Database.php @@ -34,7 +34,7 @@ class Database * the column label in which you specify a condition for the * column. * - * @return float|string + * @return null|float|string */ public static function DAVERAGE($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DAverage.php b/src/PhpSpreadsheet/Calculation/Database/DAverage.php index 899b2426..ea45beda 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DAverage.php +++ b/src/PhpSpreadsheet/Calculation/Database/DAverage.php @@ -29,7 +29,7 @@ class DAverage extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float|string + * @return null|float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DCountA.php b/src/PhpSpreadsheet/Calculation/Database/DCountA.php index 5e2ef110..1beaf012 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DCountA.php +++ b/src/PhpSpreadsheet/Calculation/Database/DCountA.php @@ -36,7 +36,7 @@ class DCountA extends DatabaseAbstract $field = self::fieldExtract($database, $field); return Statistical::COUNTA( - self::getFilteredColumn($database, $field, $criteria) + self::getFilteredColumn($database, $field ?? 0, $criteria) ); } } diff --git a/src/PhpSpreadsheet/Calculation/Database/DGet.php b/src/PhpSpreadsheet/Calculation/Database/DGet.php index 64858d9d..c2ffe302 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DGet.php +++ b/src/PhpSpreadsheet/Calculation/Database/DGet.php @@ -44,6 +44,8 @@ class DGet extends DatabaseAbstract return Functions::NAN(); } - return $columnData[0]; + $row = array_pop($columnData); + + return array_pop($row); } } diff --git a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php index a08f1251..2148ebc0 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php +++ b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -25,19 +25,20 @@ abstract class DatabaseAbstract * represents the position of the column within the list: 1 for * the first column, 2 for the second column, and so on. */ - protected static function fieldExtract(array $database, $field): ?string + protected static function fieldExtract(array $database, $field): ?int { $field = strtoupper(Functions::flattenSingleValue($field)); - $fieldNames = array_map('strtoupper', array_shift($database)); - - if (is_numeric($field)) { - $keys = array_keys($fieldNames); - - return $keys[$field - 1]; + if ($field === '') { + return null; } - $key = array_search($field, $fieldNames); - return $key ?: null; + $fieldNames = array_map('strtoupper', array_shift($database)); + if (is_numeric($field)) { + return ((int) $field) - 1; + } + $key = array_search($field, array_values($fieldNames), true); + + return ($key !== false) ? (int) $key : null; } /** @@ -70,14 +71,19 @@ abstract class DatabaseAbstract return self::executeQuery($database, $query, $criteriaNames, $fieldNames); } - protected static function getFilteredColumn(array $database, $field, array $criteria): array + protected static function getFilteredColumn(array $database, ?int $field, array $criteria): array { // reduce the database to a set of rows that match all the criteria $database = self::filter($database, $criteria); + $defaultReturnColumnValue = ($field === null) ? 1 : null; + // extract an array of values for the requested column $columnData = []; - foreach ($database as $row) { - $columnData[] = ($field !== null) ? $row[$field] : true; + foreach ($database as $rowKey => $row) { + $keys = array_keys($row); + $key = $keys[$field] ?? null; + $columnKey = $key ?? 'A'; + $columnData[$rowKey][$columnKey] = $row[$key] ?? $defaultReturnColumnValue; } return $columnData; diff --git a/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php b/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php index 5b4fe5b1..2ba20346 100644 --- a/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php +++ b/src/PhpSpreadsheet/Calculation/Internal/WildcardMatch.php @@ -5,9 +5,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Internal; class WildcardMatch { private const SEARCH_SET = [ - '/([^~])(\*)/ui', + '/(? $arg) { - if (!is_numeric($arg)) { - if ($conditionIsNumeric) { - continue; - } - $arg = Calculation::wrapResult(strtoupper($arg)); - } elseif (!$conditionIsNumeric) { - continue; - } - $testCondition = '=' . $arg . $condition; - if (Calculation::getInstance()->_calculateFormulaValue($testCondition)) { - $returnValue += $averageArgs[$key]; - ++$aCount; - } - } - - if ($aCount > 0) { - return $returnValue / $aCount; - } - - return Functions::DIV0(); + return Statistical\Conditional::AVERAGEIF($range, $condition, $averageRange); } /** @@ -1137,38 +1111,21 @@ class Statistical * Counts the number of cells that contain numbers within the list of arguments * * Excel Function: - * COUNTIF(value1[,value2[, ...]],condition) + * COUNTIF(range,condition) * - * @param mixed $aArgs Data values + * @Deprecated 1.17.0 + * + * @see Statistical\Conditional::COUNTIF() + * Use the COUNTIF() method in the Statistical\Conditional class instead + * + * @param mixed $range Data values * @param string $condition the criteria that defines which cells will be counted * * @return int */ - public static function COUNTIF($aArgs, $condition) + public static function COUNTIF($range, $condition) { - $returnValue = 0; - - $aArgs = Functions::flattenArray($aArgs); - $condition = Functions::ifCondition($condition); - $conditionIsNumeric = strpos($condition, '"') === false; - // Loop through arguments - foreach ($aArgs as $arg) { - if (!is_numeric($arg)) { - if ($conditionIsNumeric) { - continue; - } - $arg = Calculation::wrapResult(strtoupper($arg)); - } elseif (!$conditionIsNumeric) { - continue; - } - $testCondition = '=' . $arg . $condition; - if (Calculation::getInstance()->_calculateFormulaValue($testCondition)) { - // Is it a value within our criteria - ++$returnValue; - } - } - - return $returnValue; + return Statistical\Conditional::COUNTIF($range, $condition); } /** @@ -1179,66 +1136,18 @@ class Statistical * Excel Function: * COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2]…) * - * @param mixed $args Criterias + * @Deprecated 1.17.0 + * + * @see Statistical\Conditional::COUNTIFS() + * Use the COUNTIFS() method in the Statistical\Conditional class instead + * + * @param mixed $args Pairs of Ranges and Criteria * * @return int */ public static function COUNTIFS(...$args) { - $arrayList = $args; - - // Return value - $returnValue = 0; - - if (empty($arrayList)) { - return $returnValue; - } - - $aArgsArray = []; - $conditions = []; - - while (count($arrayList) > 0) { - $aArgsArray[] = Functions::flattenArray(array_shift($arrayList)); - $conditions[] = Functions::ifCondition(array_shift($arrayList)); - } - - // Loop through each arg and see if arguments and conditions are true - foreach (array_keys($aArgsArray[0]) as $index) { - $valid = true; - - foreach ($conditions as $cidx => $condition) { - $conditionIsNumeric = strpos($condition, '"') === false; - $arg = $aArgsArray[$cidx][$index]; - - // Loop through arguments - if (!is_numeric($arg)) { - if ($conditionIsNumeric) { - $valid = false; - - break; // if false found, don't need to check other conditions - } - $arg = Calculation::wrapResult(strtoupper($arg)); - } elseif (!$conditionIsNumeric) { - $valid = false; - - break; // if false found, don't need to check other conditions - } - $testCondition = '=' . $arg . $condition; - if (!Calculation::getInstance()->_calculateFormulaValue($testCondition)) { - // Is not a value within our criteria - $valid = false; - - break; // if false found, don't need to check other conditions - } - } - - if ($valid) { - ++$returnValue; - } - } - - // Return - return $returnValue; + return Statistical\Conditional::COUNTIFS(...$args); } /** @@ -2348,53 +2257,18 @@ class Statistical * Excel Function: * MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) * + * @Deprecated 1.17.0 + * + * @see Statistical\Conditional::MAXIFS() + * Use the MAXIFS() method in the Statistical\Conditional class instead + * * @param mixed $args Data range and criterias * * @return float */ public static function MAXIFS(...$args) { - $arrayList = $args; - - // Return value - $returnValue = null; - - $maxArgs = Functions::flattenArray(array_shift($arrayList)); - $aArgsArray = []; - $conditions = []; - - while (count($arrayList) > 0) { - $aArgsArray[] = Functions::flattenArray(array_shift($arrayList)); - $conditions[] = Functions::ifCondition(array_shift($arrayList)); - } - - // Loop through each arg and see if arguments and conditions are true - foreach ($maxArgs as $index => $value) { - $valid = true; - - foreach ($conditions as $cidx => $condition) { - $arg = $aArgsArray[$cidx][$index]; - - // Loop through arguments - if (!is_numeric($arg)) { - $arg = Calculation::wrapResult(strtoupper($arg)); - } - $testCondition = '=' . $arg . $condition; - if (!Calculation::getInstance()->_calculateFormulaValue($testCondition)) { - // Is not a value within our criteria - $valid = false; - - break; // if false found, don't need to check other conditions - } - } - - if ($valid) { - $returnValue = $returnValue === null ? $value : max($value, $returnValue); - } - } - - // Return - return $returnValue; + return Conditional::MAXIFS(...$args); } /** @@ -2520,53 +2394,18 @@ class Statistical * Excel Function: * MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) * + * @Deprecated 1.17.0 + * + * @see Statistical\Conditional::MINIFS() + * Use the MINIFS() method in the Statistical\Conditional class instead + * * @param mixed $args Data range and criterias * * @return float */ public static function MINIFS(...$args) { - $arrayList = $args; - - // Return value - $returnValue = null; - - $minArgs = Functions::flattenArray(array_shift($arrayList)); - $aArgsArray = []; - $conditions = []; - - while (count($arrayList) > 0) { - $aArgsArray[] = Functions::flattenArray(array_shift($arrayList)); - $conditions[] = Functions::ifCondition(array_shift($arrayList)); - } - - // Loop through each arg and see if arguments and conditions are true - foreach ($minArgs as $index => $value) { - $valid = true; - - foreach ($conditions as $cidx => $condition) { - $arg = $aArgsArray[$cidx][$index]; - - // Loop through arguments - if (!is_numeric($arg)) { - $arg = Calculation::wrapResult(strtoupper($arg)); - } - $testCondition = '=' . $arg . $condition; - if (!Calculation::getInstance()->_calculateFormulaValue($testCondition)) { - // Is not a value within our criteria - $valid = false; - - break; // if false found, don't need to check other conditions - } - } - - if ($valid) { - $returnValue = $returnValue === null ? $value : min($value, $returnValue); - } - } - - // Return - return $returnValue; + return Conditional::MINIFS(...$args); } // diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php new file mode 100644 index 00000000..02bb0782 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php @@ -0,0 +1,234 @@ + 0) { + $conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [array_pop($args)]); + $database[] = array_merge( + [sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], + Functions::flattenArray(array_pop($args)) + ); + ++$pairCount; + } + + $conditions = array_map(null, ...$conditions); + $database = array_map(null, ...$database); + + return DCount::evaluate($database, null, $conditions); + } + + /** + * MAXIFS. + * + * Returns the maximum value within a range of cells that contain numbers within the list of arguments + * + * Excel Function: + * MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2]…) + * + * @param mixed $args Pairs of Ranges and Criteria + * + * @return null|float|string + */ + public static function MAXIFS(...$args) + { + if (empty($args)) { + return 0.0; + } + + $conditions = self::buildConditionSet(...$args); + $database = self::buildDatabase(...$args); + + return DMax::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); + } + + /** + * MINIFS. + * + * Returns the minimum value within a range of cells that contain numbers within the list of arguments + * + * Excel Function: + * MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2]…) + * + * @param mixed $args Pairs of Ranges and Criteria + * + * @return null|float|string + */ + public static function MINIFS(...$args) + { + if (empty($args)) { + return 0.0; + } + + $conditions = self::buildConditionSet(...$args); + $database = self::buildDatabase(...$args); + + return DMin::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); + } + + private static function buildConditionSet(...$args): array + { + array_shift($args); + + $conditions = []; + $pairCount = 1; + while (count($args) > 0) { + $conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [array_pop($args)]); + array_pop($args); + ++$pairCount; + } + + if (count($conditions) === 1) { + return array_map( + function ($value) { + return [$value]; + }, + $conditions[0] + ); + } + + return array_map(null, ...$conditions); + } + + private static function buildDatabase(...$args): array + { + $database = []; + $database[] = array_merge( + [self::VALUE_COLUMN_NAME], + Functions::flattenArray(array_shift($args)) + ); + + $pairCount = 1; + while (count($args) > 0) { + array_pop($args); + $database[] = array_merge( + [sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], + Functions::flattenArray(array_pop($args)) + ); + ++$pairCount; + } + + return array_map(null, ...$database); + } +} diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 02305b7a..19119970 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -824,7 +824,7 @@ class Worksheet implements IComparable /** * Set title. * - * @param string $pValue String containing the dimension of this worksheet + * @param string $title String containing the dimension of this worksheet * @param bool $updateFormulaCellReferences Flag indicating whether cell references in formulae should * be updated to reflect the new sheet name. * This should be left as the default true, unless you are @@ -835,10 +835,10 @@ class Worksheet implements IComparable * * @return $this */ - public function setTitle($pValue, $updateFormulaCellReferences = true, $validate = true) + public function setTitle($title, $updateFormulaCellReferences = true, $validate = true) { // Is this a 'rename' or not? - if ($this->getTitle() == $pValue) { + if ($this->getTitle() == $title) { return $this; } @@ -847,37 +847,37 @@ class Worksheet implements IComparable if ($validate) { // Syntax check - self::checkSheetTitle($pValue); + self::checkSheetTitle($title); if ($this->parent) { // Is there already such sheet name? - if ($this->parent->sheetNameExists($pValue)) { + if ($this->parent->sheetNameExists($title)) { // Use name, but append with lowest possible integer - if (Shared\StringHelper::countCharacters($pValue) > 29) { - $pValue = Shared\StringHelper::substring($pValue, 0, 29); + if (Shared\StringHelper::countCharacters($title) > 29) { + $title = Shared\StringHelper::substring($title, 0, 29); } $i = 1; - while ($this->parent->sheetNameExists($pValue . ' ' . $i)) { + while ($this->parent->sheetNameExists($title . ' ' . $i)) { ++$i; if ($i == 10) { - if (Shared\StringHelper::countCharacters($pValue) > 28) { - $pValue = Shared\StringHelper::substring($pValue, 0, 28); + if (Shared\StringHelper::countCharacters($title) > 28) { + $title = Shared\StringHelper::substring($title, 0, 28); } } elseif ($i == 100) { - if (Shared\StringHelper::countCharacters($pValue) > 27) { - $pValue = Shared\StringHelper::substring($pValue, 0, 27); + if (Shared\StringHelper::countCharacters($title) > 27) { + $title = Shared\StringHelper::substring($title, 0, 27); } } } - $pValue .= " $i"; + $title .= " $i"; } } } // Set title - $this->title = $pValue; + $this->title = $title; $this->dirty = true; if ($this->parent && $this->parent->getCalculationEngine()) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php index 4cfea42f..f5214ed0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DCountATest.php @@ -80,10 +80,6 @@ class DCountATest extends TestCase ['Science', 'Male'], ], ], - /* - * Null value in datacolumn behaviour for DCOUNTA... will include not include a null value in the count - * if it is an actual cell value; but it will be included if it is a literal... this test case is - * currently passing literals [ 1, $this->database2(), @@ -93,7 +89,6 @@ class DCountATest extends TestCase ['Math', 'Female'], ], ], - */ [ 3, $this->database2(), diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php index 10915b3c..5c270fd2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Database/DSumTest.php @@ -103,6 +103,15 @@ class DSumTest extends TestCase ['3', 'C*'], ], ], + [ + 705000, + $this->database2(), + 'Sales', + [ + ['Quarter', 'Sales Rep.'], + ['3', '<>C*'], + ], + ], [ null, $this->database1(), diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/AverageIfsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/AverageIfsTest.php new file mode 100644 index 00000000..84c53431 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/AverageIfsTest.php @@ -0,0 +1,31 @@ +getSamples() as $samples) { foreach ($samples as $sample) { +// if (array_pop(explode('/', $sample)) !== 'DGET.php') { +// continue; +// } if (!in_array($sample, $skipped)) { $file = 'samples/' . $sample; $result[] = [$file]; diff --git a/tests/data/Calculation/Statistical/AVERAGEIF.php b/tests/data/Calculation/Statistical/AVERAGEIF.php index 422a2771..736b833f 100644 --- a/tests/data/Calculation/Statistical/AVERAGEIF.php +++ b/tests/data/Calculation/Statistical/AVERAGEIF.php @@ -46,8 +46,39 @@ return [ '<2013', ], [ - 14000, - [7000, 14000, 'Hello World', 21000, 28000], + 200, + [7000, 14000, 21000, 28000], '<23000', + [100, 200, 300, 800], + ], + [ + (2 + 4 + 8) / 3, + [true, true, false, true, false], + true, + [2, 4, 6, 8, 10], + ], + [ + (6 + 10) / 2, + [true, true, false, true, false], + '<>true', + [2, 4, 6, 8, 10], + ], + [ + (1 + 2 + 5 + 6) / 4, + ['North', 'South', 'East', 'West', 'North', 'South', 'East', 'West'], + '???th', + [1, 2, 3, 4, 5, 6, 7, 8], + ], + [ + 16733.5, + ['East', 'West', 'North', 'South (New Office)', 'Midwest'], + '=*West', + [45678, 23789, -4789, 0, 9678], + ], + [ + 18589, + ['East', 'West', 'North', 'South (New Office)', 'Midwest'], + '<>*(New Office)', + [45678, 23789, -4789, 0, 9678], ], ]; diff --git a/tests/data/Calculation/Statistical/AVERAGEIFS.php b/tests/data/Calculation/Statistical/AVERAGEIFS.php new file mode 100644 index 00000000..6c94300c --- /dev/null +++ b/tests/data/Calculation/Statistical/AVERAGEIFS.php @@ -0,0 +1,42 @@ +70', + [75, 94, 86, 'incomplete'], + '<90', + ], + [ + '#DIV/0!', + [85, 80, 93, 75], + [85, 80, 93, 75], + '>95', + ], + [ + 87.5, + [87, 88, 'incomplete', 75], + [87, 88, 'incomplete', 75], + '<>incomplete', + [87, 88, 'incomplete', 75], + '>80', + ], + [ + 174000, + [223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000], + [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], + 1, + ['North', 'North', 'South', 'North', 'North', 'South', 'North', 'North', 'South', 'North', 'North', 'South'], + 'North', + ], + [ + 285500, + [223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000], + [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], + '>2', + ['Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol'], + 'Jeff', + ], +]; diff --git a/tests/data/Calculation/Statistical/COUNTIF.php b/tests/data/Calculation/Statistical/COUNTIF.php index c0e69c67..69277e72 100644 --- a/tests/data/Calculation/Statistical/COUNTIF.php +++ b/tests/data/Calculation/Statistical/COUNTIF.php @@ -23,12 +23,55 @@ return [ ], [ 2, - [6, 3, 4, 'X', ''], + [6, 3, 4, 'X', '', null], '<=4', ], [ 2, - [6, 3, 4, 'X', ''], + [6, 3, 4, 31, 'X', '', null], '<="4"', ], + [ + 2, + [0, 1, 1, 2, 3, 5, 8, 0, 13, 21], + 0, + ], + [ + 3, + [true, false, false, true, false, true, false, false], + true, + ], + [ + 5, + [true, false, false, true, false, true, false, false], + '<>true', + ], + [ + 4, + ['apples', 'oranges', 'peaches', 'apples'], + '*', + ], + [ + 3, + ['apples', 'oranges', 'peaches', 'apples'], + '*p*s*', + ], + [ + 4, + [ + ['apples', 'oranges', 'peaches', 'apples'], + ['bananas', 'mangoes', 'grapes', 'cherries'], + ], + '*p*e*', + ], + [ + 2, + ['apples', 'oranges', 'peaches', 'apples'], + '?????es', + ], + [ + 2, + ['great * ratings', 'bad * ratings', 'films * wars', 'films * trek', 'music * radio'], + '*~* ra*s', + ], ]; diff --git a/tests/data/Calculation/Statistical/COUNTIFS.php b/tests/data/Calculation/Statistical/COUNTIFS.php index 32f64d71..07f96d38 100644 --- a/tests/data/Calculation/Statistical/COUNTIFS.php +++ b/tests/data/Calculation/Statistical/COUNTIFS.php @@ -16,9 +16,28 @@ return [ ['C', 'B', 'A', 'B', 'B'], '=B', ], + // [ + // 2, + // [1, 2, 3, 'B', null, '', false], + // '<=2', + // ], + // [ + // 2, + // [1, 2, 3, 'B', null, '', false], + // '<=B', + // ], + [ + 4, + ['Female', 'Female', 'Female', 'Male', 'Male', 'Male', 'Female', 'Female', 'Female', 'Male', 'Male', 'Male'], + 'Female', + [0.63, 0.78, 0.39, 0.55, 0.71, 0.51, 0.78, 0.81, 0.49, 0.35, 0.69, 0.65], + '>60%', + ], [ 2, - [1, 2, 3, 'B', '', false], - '<=2', + ['Maths', 'English', 'Science', 'Maths', 'English', 'Science', 'Maths', 'English', 'Science', 'Maths', 'English', 'Science'], + 'Science', + [0.63, 0.78, 0.39, 0.55, 0.71, 0.51, 0.78, 0.81, 0.49, 0.35, 0.69, 0.65], + '<50%', ], ]; diff --git a/tests/data/Calculation/Statistical/MAXIFS.php b/tests/data/Calculation/Statistical/MAXIFS.php index deb4bceb..58ed5e46 100644 --- a/tests/data/Calculation/Statistical/MAXIFS.php +++ b/tests/data/Calculation/Statistical/MAXIFS.php @@ -21,6 +21,20 @@ return [ ], '=H', ], + [ + 2, + [ + [1], + [2], + [3], + ], + [ + ['Y'], + ['Y'], + ['N'], + ], + '=Y', + ], [ 2, [ @@ -41,4 +55,18 @@ return [ ], '=B', ], + [ + 456000, + [223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000], + [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], + 1, + ], + [ + 310000, + [223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000], + [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], + '>2', + ['Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol'], + 'Jeff', + ], ]; diff --git a/tests/data/Calculation/Statistical/MINIFS.php b/tests/data/Calculation/Statistical/MINIFS.php index a00f25b7..6ecade9f 100644 --- a/tests/data/Calculation/Statistical/MINIFS.php +++ b/tests/data/Calculation/Statistical/MINIFS.php @@ -21,6 +21,20 @@ return [ ], '=H', ], + [ + 1, + [ + [1], + [2], + [3], + ], + [ + ['Y'], + ['Y'], + ['N'], + ], + '=Y', + ], [ 2, [ @@ -41,4 +55,18 @@ return [ ], '=B', ], + [ + 125000, + [223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000], + [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], + 1, + ], + [ + 261000, + [223000, 125000, 456000, 322000, 340000, 198000, 310000, 250000, 460000, 261000, 389000, 305000], + [1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4], + '>2', + ['Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol'], + 'Jeff', + ], ]; From 80a20fc9912eb4fc0cd010fa943038a9686af412 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sat, 27 Feb 2021 11:43:22 -0800 Subject: [PATCH 07/89] 100% Coverage for Calculation/DateTime (#1870) * 100% Coverage for Calculation/DateTime The code in DateTime is now completely covered. Along the way, some errors were discovered and corrected. - The tests which have had to be changed at the start of every year are replaced by more robust equivalents which do not require annual changes. - Several places in the code where Gnumeric and OpenOffice were thought to differ from Excel do not appear to have had any justification. I have left a comment where such code has been removed. - Use DateTime when possible rather than date, time, or strftime functions to avoid potential Y2038 problems. - Some impossible code has been removed, replaced by an explanatory comment. - NETWORKDAYS had a bug when the start date was Sunday. There had been no tests of this condition. - Some functions allow boolean and null arguments where a number is expected. This is more complicated than the equivalent situations in MathTrig because the initial date for these calculations can be Day 1 rather than Day 0. - More testing for dates from 1900-01-01 through the fictitious everywhere-but-Excel 1900-01-29. - This showed that there is an additional Excel bug - Excel evaluates WEEKNUM(emptycell) as 0, which is not a valid result for WEEKNUM without a second argument. PhpSpreadsheet now duplicates this bug. - There is a similar and even worse bug for 1904-01-01 in 1904 calculations. Weeknum returns 0 for this, but returns the correct value for arguments of 0 or null. - DATEVALUE should accept 1900-02-29 (sigh) and relatives. PhpSpreadsheet now duplicates this bug. - Testing bootstrap sets default timezone. This appears to be a relic from the releases of PHP where the unwise decision, subsequenly reversed, was made to issue messages for "no default timezone is set" rather than just use a sensible default. This was a disruptive setting for some of the tests I added. There is only one test in the entire suite which is default-timezone-dependent. Setting and resetting of default timezone is moved to that test (Reader/ODS/ODSTest), and out of bootstrap. - There had been no testing of NOW() function. - DATEVALUE test had no tests for 1904 calendar and needs some. - DATE test changed 1900/1904 calendar in use without restoring it. - WEEKDAY test had no tests for 1904 calendar and needs some. - Which revealed a bug in Shared/Date (excelToDateTimeObject was not recognizing 1904-01-01 as valid when 1904 calendar is in use). - And an additional bug in that legal 1904-calendar values in the 0.0-1.0 range yielded the same "wrong" answers as 1900-calendar (see "One note" below). Also the comment for one of the calendar-1904 tests was wrong in attempting to identify what time of day the fraction represented. I had wanted to break this up into a set of smaller modules, a process already started for Engineering and MathTrig. However the number of source code changes was sufficient that I wanted a clean delta for this request. If it is merged, I will work on breaking it up afterwards. One note - Shared/Date/excelToDateTimeObject, when calendar-1900 is in use, returns an unexpected result if its argument is between 0 and 1, which is nominally invalid for that calendar. It uses a base-1970 calendar in that instance. That check is not justifiable for calendar-1904, where values in that range are legal, so I made the check specific to calendar-1900, and adjusted 3 1904 unit test results accordingly. However, I have to admit that I don't understand why that check should be made even for calendar-1900. It certainly doesn't match anything that Excel does. I would recommend scrapping that code altogether. If agreed, I would do this as part of the break-up into smaller modules. Another note - more controversially, it is clear that PhpSpreadsheet needs to support the Excel and PHP date formats. Although it requires further study, I am not convinced that it needs to support Unix timestamp format. Since that is a potential source of Y2038 problems on 32-bit systems, I would like to open a PR to deprecate the use of that format. Please let me know if you are aware of a valid reason to continue to support it. --- src/PhpSpreadsheet/Calculation/DateTime.php | 450 +++++++++--------- src/PhpSpreadsheet/Shared/Date.php | 2 +- .../Functions/DateTime/DateTest.php | 14 +- .../Functions/DateTime/DateValueTest.php | 40 +- .../Functions/DateTime/NowTest.php | 38 ++ .../Functions/DateTime/TimeTest.php | 32 +- .../Functions/DateTime/WeekDayTest.php | 20 +- .../Functions/DateTime/WeekNumTest.php | 22 +- .../Reader/Ods/OdsTest.php | 19 +- tests/bootstrap.php | 2 +- tests/data/Calculation/DateTime/DATEVALUE.php | 43 +- tests/data/Calculation/DateTime/DAY.php | 15 + .../data/Calculation/DateTime/ISOWEEKNUM.php | 10 + .../data/Calculation/DateTime/NETWORKDAYS.php | 18 + tests/data/Calculation/DateTime/WEEKDAY.php | 7 + tests/data/Calculation/DateTime/WEEKNUM.php | 27 ++ tests/data/Calculation/DateTime/WORKDAY.php | 16 + tests/data/Calculation/DateTime/YEARFRAC.php | 3 +- .../data/Shared/Date/ExcelToTimestamp1904.php | 8 +- 19 files changed, 536 insertions(+), 250 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index 4c2b108a..64d72c2b 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -144,26 +144,10 @@ class DateTime */ public static function DATETIMENOW() { - $saveTimeZone = date_default_timezone_get(); - date_default_timezone_set('UTC'); - $retValue = false; - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - $retValue = (float) Date::PHPToExcel(time()); + $dti = new DateTimeImmutable(); + $dateArray = date_parse($dti->format('c')); - break; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - $retValue = (int) time(); - - break; - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - $retValue = new \DateTime(); - - break; - } - date_default_timezone_set($saveTimeZone); - - return $retValue; + return is_array($dateArray) ? self::returnIn3FormatsArray($dateArray) : Functions::VALUE(); } /** @@ -185,27 +169,10 @@ class DateTime */ public static function DATENOW() { - $saveTimeZone = date_default_timezone_get(); - date_default_timezone_set('UTC'); - $retValue = false; - $excelDateTime = floor(Date::PHPToExcel(time())); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - $retValue = (float) $excelDateTime; + $dti = new DateTimeImmutable(); + $dateArray = date_parse($dti->format('c')); - break; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - $retValue = (int) Date::excelToTimestamp($excelDateTime); - - break; - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - $retValue = Date::excelToDateTimeObject($excelDateTime); - - break; - } - date_default_timezone_set($saveTimeZone); - - return $retValue; + return is_array($dateArray) ? self::returnIn3FormatsArray($dateArray, true) : Functions::VALUE(); } /** @@ -316,14 +283,8 @@ class DateTime // Execute function $excelDateValue = Date::formattedPHPToExcel($year, $month, $day); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $excelDateValue; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp($excelDateValue); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return Date::excelToDateTimeObject($excelDateValue); - } + + return self::returnIn3FormatsFloat($excelDateValue); } /** @@ -403,36 +364,24 @@ class DateTime } // Execute function - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - $date = 0; - $calendar = Date::getExcelCalendar(); - if ($calendar != Date::CALENDAR_WINDOWS_1900) { - $date = 1; - } + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $date = 0; + $calendar = Date::getExcelCalendar(); + if ($calendar != Date::CALENDAR_WINDOWS_1900) { + $date = 1; + } - return (float) Date::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp(Date::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - $dayAdjust = 0; - if ($hour < 0) { - $dayAdjust = floor($hour / 24); - $hour = 24 - abs($hour % 24); - if ($hour == 24) { - $hour = 0; - } - } elseif ($hour >= 24) { - $dayAdjust = floor($hour / 24); - $hour = $hour % 24; - } - $phpDateObject = new \DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); - if ($dayAdjust != 0) { - $phpDateObject->modify($dayAdjust . ' days'); - } - - return $phpDateObject; + return (float) Date::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) Date::excelToTimestamp(Date::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 + } + // RETURNDATE_PHP_DATETIME_OBJECT + // Hour has already been normalized (0-23) above + $phpDateObject = new \DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); + + return $phpDateObject; } /** @@ -462,6 +411,8 @@ class DateTime */ public static function DATEVALUE($dateValue = 1) { + $dti = new DateTimeImmutable(); + $baseYear = Date::getExcelCalendar(); $dateValue = trim(Functions::flattenSingleValue($dateValue), '"'); // Strip any ordinals because they're allowed in Excel (English only) $dateValue = preg_replace('/(\d)(st|nd|rd|th)([ -\/])/Ui', '$1$3', $dateValue); @@ -470,6 +421,7 @@ class DateTime $yearFound = false; $t1 = explode(' ', $dateValue); + $t = ''; foreach ($t1 as &$t) { if ((is_numeric($t)) && ($t > 31)) { if ($yearFound) { @@ -481,10 +433,11 @@ class DateTime $yearFound = true; } } - if ((count($t1) == 1) && (strpos($t, ':') !== false)) { + if (count($t1) === 1) { // We've been fed a time value without any date - return 0.0; - } elseif (count($t1) == 2) { + return ((strpos($t, ':') === false)) ? Functions::Value() : 0.0; + } + if (count($t1) == 2) { // We only have two parts of the date: either day/month or month/year if ($yearFound) { array_unshift($t1, 1); @@ -493,7 +446,7 @@ class DateTime $t1[1] += 1900; array_unshift($t1, 1); } else { - $t1[] = date('Y'); + $t1[] = $dti->format('Y'); } } } @@ -502,23 +455,13 @@ class DateTime $PHPDateArray = date_parse($dateValue); if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { + // If original count was 1, we've already returned. + // If it was 2, we added another. + // Therefore, neither of the first 2 stroks below can fail. $testVal1 = strtok($dateValue, '- '); - if ($testVal1 !== false) { - $testVal2 = strtok('- '); - if ($testVal2 !== false) { - $testVal3 = strtok('- '); - if ($testVal3 === false) { - $testVal3 = strftime('%Y'); - } - } else { - return Functions::VALUE(); - } - } else { - return Functions::VALUE(); - } - if ($testVal1 < 31 && $testVal2 < 12 && $testVal3 < 12 && strlen($testVal3) == 2) { - $testVal3 += 2000; - } + $testVal2 = strtok('- '); + $testVal3 = strtok('- ') ?: $dti->format('Y'); + self::adjustYear($testVal1, $testVal2, $testVal3); $PHPDateArray = date_parse($testVal1 . '-' . $testVal2 . '-' . $testVal3); if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { $PHPDateArray = date_parse($testVal2 . '-' . $testVal1 . '-' . $testVal3); @@ -528,44 +471,126 @@ class DateTime } } + $retValue = Functions::Value(); if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { // Execute function - if ($PHPDateArray['year'] == '') { - $PHPDateArray['year'] = strftime('%Y'); - } - if ($PHPDateArray['year'] < 1900) { + self::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y')); + if ($PHPDateArray['year'] < $baseYear) { return Functions::VALUE(); } - if ($PHPDateArray['month'] == '') { - $PHPDateArray['month'] = strftime('%m'); - } - if ($PHPDateArray['day'] == '') { - $PHPDateArray['day'] = strftime('%d'); - } - if (!checkdate($PHPDateArray['month'], $PHPDateArray['day'], $PHPDateArray['year'])) { - return Functions::VALUE(); - } - $excelDateValue = floor( - Date::formattedPHPToExcel( - $PHPDateArray['year'], - $PHPDateArray['month'], - $PHPDateArray['day'], - $PHPDateArray['hour'], - $PHPDateArray['minute'], - $PHPDateArray['second'] - ) - ); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $excelDateValue; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp($excelDateValue); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return new \DateTime($PHPDateArray['year'] . '-' . $PHPDateArray['month'] . '-' . $PHPDateArray['day'] . ' 00:00:00'); + self::replaceIfEmpty($PHPDateArray['month'], $dti->format('m')); + self::replaceIfEmpty($PHPDateArray['day'], $dti->format('d')); + $PHPDateArray['hour'] = 0; + $PHPDateArray['minute'] = 0; + $PHPDateArray['second'] = 0; + $month = (int) $PHPDateArray['month']; + $day = (int) $PHPDateArray['day']; + $year = (int) $PHPDateArray['year']; + if (!checkdate($month, $day, $year)) { + return ($year === 1900 && $month === 2 && $day === 29) ? self::returnIn3FormatsFloat(60.0) : Functions::VALUE(); } + $retValue = is_array($PHPDateArray) ? self::returnIn3FormatsArray($PHPDateArray, true) : Functions::VALUE(); } - return Functions::VALUE(); + return $retValue; + } + + /** + * Help reduce perceived complexity of some tests. + * + * @param mixed $value + * @param mixed $altValue + */ + private static function replaceIfEmpty(&$value, $altValue): void + { + $value = $value ?: $altValue; + } + + /** + * Adjust year in ambiguous situations. + */ + private static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void + { + if (!is_numeric($testVal1) || $testVal1 < 31) { + if (!is_numeric($testVal2) || $testVal2 < 12) { + if (is_numeric($testVal3) && $testVal3 < 12) { + $testVal3 += 2000; + } + } + } + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + private static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return new \DateTime( + $dateArray['year'] + . '-' . $dateArray['month'] + . '-' . $dateArray['day'] + . ' ' . $dateArray['hour'] + . ':' . $dateArray['minute'] + . ':' . $dateArray['second'] + ); + } + $excelDateValue = + Date::formattedPHPToExcel( + $dateArray['year'], + $dateArray['month'], + $dateArray['day'], + $dateArray['hour'], + $dateArray['minute'], + $dateArray['second'] + ); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $noFrac ? floor($excelDateValue) : (float) $excelDateValue; + } + // RETURNDATE_UNIX_TIMESTAMP) + + return (int) Date::excelToTimestamp($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + private static function returnIn3FormatsFloat(float $excelDateValue) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $excelDateValue; + } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) Date::excelToTimestamp($excelDateValue); + } + // RETURNDATE_PHP_DATETIME_OBJECT + + return Date::excelToDateTimeObject($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + private static function returnIn3FormatsObject(\DateTime $PHPDateObject) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return $PHPDateObject; + } + if ($retType === Functions::RETURNDATE_EXCEL) { + return (float) Date::PHPToExcel($PHPDateObject); + } + // RETURNDATE_UNIX_TIMESTAMP + + return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); } /** @@ -601,31 +626,22 @@ class DateTime } $PHPDateArray = date_parse($timeValue); + $retValue = Functions::VALUE(); if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE) { - $excelDateValue = Date::formattedPHPToExcel( - $PHPDateArray['year'], - $PHPDateArray['month'], - $PHPDateArray['day'], - $PHPDateArray['hour'], - $PHPDateArray['minute'], - $PHPDateArray['second'] - ); - } else { - $excelDateValue = Date::formattedPHPToExcel(1900, 1, 1, $PHPDateArray['hour'], $PHPDateArray['minute'], $PHPDateArray['second']) - 1; - } + // OpenOffice-specific code removed - it works just like Excel + $excelDateValue = Date::formattedPHPToExcel(1900, 1, 1, $PHPDateArray['hour'], $PHPDateArray['minute'], $PHPDateArray['second']) - 1; - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $excelDateValue; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) $phpDateValue = Date::excelToTimestamp($excelDateValue + 25569) - 3600; - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return new \DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $retValue = (float) $excelDateValue; + } elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + $retValue = (int) $phpDateValue = Date::excelToTimestamp($excelDateValue + 25569) - 3600; + } else { + $retValue = new \DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); } } - return Functions::VALUE(); + return $retValue; } /** @@ -980,7 +996,7 @@ class DateTime // Execute function $startDoW = 6 - self::WEEKDAY($startDate, 2); if ($startDoW < 0) { - $startDoW = 0; + $startDoW = 5; } $endDoW = self::WEEKDAY($endDate, 2); if ($endDoW >= 6) { @@ -1113,14 +1129,7 @@ class DateTime } } - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) $endDate; - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp($endDate); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return Date::excelToDateTimeObject($endDate); - } + return self::returnIn3FormatsFloat($endDate); } /** @@ -1141,9 +1150,10 @@ class DateTime { $dateValue = Functions::flattenSingleValue($dateValue); - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { + if ($dateValue === null || is_bool($dateValue)) { + return (int) $dateValue; + } + if (is_string($dateValue = self::getDateValue($dateValue))) { return Functions::VALUE(); } @@ -1170,7 +1180,7 @@ class DateTime * Excel Function: * WEEKDAY(dateValue[,style]) * - * @param int $dateValue Excel date serial value (float), PHP date timestamp (integer), + * @param float|int|string $dateValue Excel date serial value (float), PHP date timestamp (integer), * PHP DateTime object, or a standard date string * @param int $style A number that determines the type of return value * 1 or omitted Numbers 1 (Sunday) through 7 (Saturday). @@ -1182,6 +1192,7 @@ class DateTime public static function WEEKDAY($dateValue = 1, $style = 1) { $dateValue = Functions::flattenSingleValue($dateValue); + self::nullFalseTrueToNumber($dateValue); $style = Functions::flattenSingleValue($style); if (!is_numeric($style)) { @@ -1191,19 +1202,19 @@ class DateTime } $style = floor($style); - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { + $dateValue = self::getDateValue($dateValue); + if (is_string($dateValue)) { return Functions::VALUE(); - } elseif ($dateValue < 0.0) { + } + if ($dateValue < 0.0) { return Functions::NAN(); } // Execute function $PHPDateObject = Date::excelToDateTimeObject($dateValue); + self::silly1900($PHPDateObject); $DoW = (int) $PHPDateObject->format('w'); - $firstDay = 1; switch ($style) { case 1: ++$DoW; @@ -1219,20 +1230,10 @@ class DateTime if ($DoW === 0) { $DoW = 7; } - $firstDay = 0; --$DoW; break; } - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { - // Test for Excel's 1900 leap year, and introduce the error as required - if (($PHPDateObject->format('Y') == 1900) && ($PHPDateObject->format('n') <= 2)) { - --$DoW; - if ($DoW < $firstDay) { - $DoW += 7; - } - } - } return $DoW; } @@ -1298,17 +1299,26 @@ class DateTime */ public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) { + $origDateValueNull = $dateValue === null; $dateValue = Functions::flattenSingleValue($dateValue); $method = Functions::flattenSingleValue($method); - if (!is_numeric($method)) { return Functions::VALUE(); } + $method = (int) $method; if (!array_key_exists($method, self::METHODARR)) { return Functions::NaN(); } $method = self::METHODARR[$method]; + if ($dateValue === null) { // boolean not allowed + // This seems to be an additional Excel bug. + if (self::buggyWeekNum1900($method)) { + return 0; + } + //$dateValue = 1; + $dateValue = (Date::getExcelCalendar() === DATE::CALENDAR_MAC_1904) ? 0 : 1; + } $dateValue = self::getDateValue($dateValue); if (is_string($dateValue)) { @@ -1321,8 +1331,14 @@ class DateTime // Execute function $PHPDateObject = Date::excelToDateTimeObject($dateValue); if ($method == self::STARTWEEK_MONDAY_ISO) { + self::silly1900($PHPDateObject); + return (int) $PHPDateObject->format('W'); } + if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) { + return 0; + } + self::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches $dayOfYear = $PHPDateObject->format('z'); $PHPDateObject->modify('-' . $dayOfYear . ' days'); $firstDayOfFirstWeek = $PHPDateObject->format('w'); @@ -1334,6 +1350,18 @@ class DateTime return (int) $weekOfYear; } + private static function buggyWeekNum1900(int $method): bool + { + return $method === self::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_WINDOWS_1900; + } + + private static function buggyWeekNum1904(int $method, bool $origNull, \DateTime $dateObject): bool + { + // This appears to be another Excel bug. + + return $method === self::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_MAC_1904 && !$origNull && $dateObject->format('Y-m-d') === '1904-01-01'; + } + /** * ISOWEEKNUM. * @@ -1350,17 +1378,19 @@ class DateTime public static function ISOWEEKNUM($dateValue = 1) { $dateValue = Functions::flattenSingleValue($dateValue); + self::nullFalseTrueToNumber($dateValue); - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { + $dateValue = self::getDateValue($dateValue); + if (!is_numeric($dateValue)) { return Functions::VALUE(); - } elseif ($dateValue < 0.0) { + } + if ($dateValue < 0.0) { return Functions::NAN(); } // Execute function $PHPDateObject = Date::excelToDateTimeObject($dateValue); + self::silly1900($PHPDateObject); return (int) $PHPDateObject->format('W'); } @@ -1449,12 +1479,7 @@ class DateTime $timeValue = Functions::flattenSingleValue($timeValue); if (!is_numeric($timeValue)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $testVal = strtok($timeValue, '/-: '); - if (strlen($testVal) < strlen($timeValue)) { - return Functions::VALUE(); - } - } + // Gnumeric test removed - it operates like Excel $timeValue = self::getTimeValue($timeValue); if (is_string($timeValue)) { return Functions::VALUE(); @@ -1490,12 +1515,7 @@ class DateTime $timeValue = $timeTester = Functions::flattenSingleValue($timeValue); if (!is_numeric($timeValue)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $testVal = strtok($timeValue, '/-: '); - if (strlen($testVal) < strlen($timeValue)) { - return Functions::VALUE(); - } - } + // Gnumeric test removed - it operates like Excel $timeValue = self::getTimeValue($timeValue); if (is_string($timeValue)) { return Functions::VALUE(); @@ -1531,12 +1551,7 @@ class DateTime $timeValue = Functions::flattenSingleValue($timeValue); if (!is_numeric($timeValue)) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $testVal = strtok($timeValue, '/-: '); - if (strlen($testVal) < strlen($timeValue)) { - return Functions::VALUE(); - } - } + // Gnumeric test removed - it operates like Excel $timeValue = self::getTimeValue($timeValue); if (is_string($timeValue)) { return Functions::VALUE(); @@ -1590,14 +1605,7 @@ class DateTime // Execute function $PHPDateObject = self::adjustDateByMonths($dateValue, $adjustmentMonths); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) Date::PHPToExcel($PHPDateObject); - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return $PHPDateObject; - } + return self::returnIn3FormatsObject($PHPDateObject); } /** @@ -1639,13 +1647,31 @@ class DateTime $adjustDaysString = '-' . $adjustDays . ' days'; $PHPDateObject->modify($adjustDaysString); - switch (Functions::getReturnDateType()) { - case Functions::RETURNDATE_EXCEL: - return (float) Date::PHPToExcel($PHPDateObject); - case Functions::RETURNDATE_UNIX_TIMESTAMP: - return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); - case Functions::RETURNDATE_PHP_DATETIME_OBJECT: - return $PHPDateObject; + return self::returnIn3FormatsObject($PHPDateObject); + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @param mixed $number + */ + private static function nullFalseTrueToNumber(&$number): void + { + $number = Functions::flattenSingleValue($number); + $baseYear = Date::getExcelCalendar(); + $nullVal = $baseYear === DATE::CALENDAR_MAC_1904 ? 0 : 1; + if ($number === null) { + $number = $nullVal; + } elseif (is_bool($number)) { + $number = $nullVal + (int) $number; + } + } + + private static function silly1900(\DateTime $PHPDateObject, string $mod = '-1 day'): void + { + $isoDate = $PHPDateObject->format('c'); + if ($isoDate < '1900-03-01') { + $PHPDateObject->modify($mod); } } } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 180a7159..28c39255 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -160,7 +160,7 @@ class Date { $timeZone = ($timeZone === null) ? self::getDefaultTimezone() : self::validateTimeZone($timeZone); if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { - if ($excelTimestamp < 1.0) { + if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) { // Unix timestamp base date $baseDate = new \DateTime('1970-01-01', $timeZone); } else { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php index 48f7cfd7..aad59729 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php @@ -9,11 +9,21 @@ use PHPUnit\Framework\TestCase; class DateTest extends TestCase { + private $returnDateType; + + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->returnDateType = Functions::getReturnDateType(); + $this->excelCalendar = Date::getExcelCalendar(); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + } + + protected function tearDown(): void + { + Functions::setReturnDateType($this->returnDateType); + Date::setExcelCalendar($this->excelCalendar); } /** diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php index 51e4f7c0..72e036f9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; +use DateTimeImmutable; use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\DateTime; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -10,11 +11,21 @@ use PHPUnit\Framework\TestCase; class DateValueTest extends TestCase { + private $returnDateType; + + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->returnDateType = Functions::getReturnDateType(); + $this->excelCalendar = Date::getExcelCalendar(); Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + } + + protected function tearDown(): void + { + Functions::setReturnDateType($this->returnDateType); + Date::setExcelCalendar($this->excelCalendar); } /** @@ -25,7 +36,21 @@ class DateValueTest extends TestCase */ public function testDATEVALUE($expectedResult, $dateValue): void { - $result = DateTime::DATEVALUE($dateValue); + // Loop to avoid extraordinarily rare edge case where first calculation + // and second do not take place on same day. + do { + $dtStart = new DateTimeImmutable(); + $startDay = $dtStart->format('d'); + if (is_string($expectedResult)) { + $replYMD = str_replace('Y', date('Y'), $expectedResult); + if ($replYMD !== $expectedResult) { + $expectedResult = DateTime::DATEVALUE($replYMD); + } + } + $result = DateTime::DATEVALUE($dateValue); + $dtEnd = new DateTimeImmutable(); + $endDay = $dtEnd->format('d'); + } while ($startDay !== $endDay); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } @@ -55,4 +80,13 @@ class DateValueTest extends TestCase // ... with the correct value self::assertEquals($result->format('d-M-Y'), '31-Jan-2012'); } + + public function testDATEVALUEwith1904Calendar(): void + { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + self::assertEquals(5428, DateTime::DATEVALUE('1918-11-11')); + self::assertEquals(0, DateTime::DATEVALUE('1904-01-01')); + self::assertEquals('#VALUE!', DateTime::DATEVALUE('1903-12-31')); + self::assertEquals('#VALUE!', DateTime::DATEVALUE('1900-02-29')); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php new file mode 100644 index 00000000..f139f703 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php @@ -0,0 +1,38 @@ +getActiveSheet(); + // Loop to avoid rare edge case where first calculation + // and second do not take place in same second. + do { + $dtStart = new DateTimeImmutable(); + $startSecond = $dtStart->format('s'); + $sheet->setCellValue('A1', '=NOW()'); + $dtEnd = new DateTimeImmutable(); + $endSecond = $dtEnd->format('s'); + } while ($startSecond !== $endSecond); + //echo("\n"); var_dump($sheet->getCell('A1')->getCalculatedValue()); echo ("\n"); + $sheet->setCellValue('B1', '=YEAR(A1)'); + $sheet->setCellValue('C1', '=MONTH(A1)'); + $sheet->setCellValue('D1', '=DAY(A1)'); + $sheet->setCellValue('E1', '=HOUR(A1)'); + $sheet->setCellValue('F1', '=MINUTE(A1)'); + $sheet->setCellValue('G1', '=SECOND(A1)'); + self::assertEquals($dtStart->format('Y'), $sheet->getCell('B1')->getCalculatedValue()); + self::assertEquals($dtStart->format('m'), $sheet->getCell('C1')->getCalculatedValue()); + self::assertEquals($dtStart->format('d'), $sheet->getCell('D1')->getCalculatedValue()); + self::assertEquals($dtStart->format('H'), $sheet->getCell('E1')->getCalculatedValue()); + self::assertEquals($dtStart->format('i'), $sheet->getCell('F1')->getCalculatedValue()); + self::assertEquals($dtStart->format('s'), $sheet->getCell('G1')->getCalculatedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php index 344061d4..3ef58374 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php @@ -9,11 +9,20 @@ use PHPUnit\Framework\TestCase; class TimeTest extends TestCase { + private $returnDateType; + + private $calendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->returnDateType = Functions::getReturnDateType(); + $this->calendar = Date::getExcelCalendar(); + } + + protected function tearDown(): void + { + Functions::setReturnDateType($this->returnDateType); + Date::setExcelCalendar($this->calendar); } /** @@ -23,6 +32,7 @@ class TimeTest extends TestCase */ public function testTIME($expectedResult, ...$args): void { + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); $result = DateTime::TIME(...$args); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } @@ -52,4 +62,20 @@ class TimeTest extends TestCase // ... with the correct value self::assertEquals($result->format('H:i:s'), '07:30:20'); } + + public function testTIME1904(): void + { + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + $result = DateTime::TIME(0, 0, 0); + self::assertEquals(0, $result); + } + + public function testTIME1900(): void + { + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $result = DateTime::TIME(0, 0, 0); + self::assertEquals(0, $result); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php index c5b89e01..99aa6f7c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php @@ -3,17 +3,21 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Shared\Date; use PHPUnit\Framework\TestCase; class WeekDayTest extends TestCase { + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->excelCalendar = Date::getExcelCalendar(); + } + + protected function tearDown(): void + { + Date::setExcelCalendar($this->excelCalendar); } /** @@ -31,4 +35,12 @@ class WeekDayTest extends TestCase { return require 'tests/data/Calculation/DateTime/WEEKDAY.php'; } + + public function testWEEKDAYwith1904Calendar(): void + { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + self::assertEquals(7, DateTime::WEEKDAY('1904-01-02')); + self::assertEquals(6, DateTime::WEEKDAY('1904-01-01')); + self::assertEquals(6, DateTime::WEEKDAY(null)); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php index 9d8e1eb2..17119f28 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php @@ -3,17 +3,21 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Shared\Date; use PHPUnit\Framework\TestCase; class WeekNumTest extends TestCase { + private $excelCalendar; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); + $this->excelCalendar = Date::getExcelCalendar(); + } + + protected function tearDown(): void + { + Date::setExcelCalendar($this->excelCalendar); } /** @@ -31,4 +35,14 @@ class WeekNumTest extends TestCase { return require 'tests/data/Calculation/DateTime/WEEKNUM.php'; } + + public function testWEEKNUMwith1904Calendar(): void + { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + self::assertEquals(27, DateTime::WEEKNUM('2004-07-02')); + self::assertEquals(1, DateTime::WEEKNUM('1904-01-02')); + self::assertEquals(1, DateTime::WEEKNUM(null)); + // The following is a bug in Excel. + self::assertEquals(0, DateTime::WEEKNUM('1904-01-01')); + } } diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 8be1aa7c..0160f68d 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -15,6 +15,19 @@ use PHPUnit\Framework\TestCase; */ class OdsTest extends TestCase { + private $timeZone; + + protected function setUp(): void + { + $this->timeZone = date_default_timezone_get(); + date_default_timezone_set('UTC'); + } + + protected function tearDown(): void + { + date_default_timezone_set($this->timeZone); + } + /** * @var Spreadsheet */ @@ -153,13 +166,13 @@ class OdsTest extends TestCase self::assertEquals(0, $firstSheet->getCell('G10')->getValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A10')->getDataType()); // Date - self::assertEquals(22269.0, $firstSheet->getCell('A10')->getValue()); + self::assertEquals('19-Dec-60', $firstSheet->getCell('A10')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A13')->getDataType()); // Time - self::assertEquals(25569.0625, $firstSheet->getCell('A13')->getValue()); + self::assertEquals('2:30:00', $firstSheet->getCell('A13')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A15')->getDataType()); // Date + Time - self::assertEquals(22269.0625, $firstSheet->getCell('A15')->getValue()); + self::assertEquals('19-Dec-60 1:30:00', $firstSheet->getCell('A15')->getFormattedValue()); self::assertEquals(DataType::TYPE_NUMERIC, $firstSheet->getCell('A11')->getDataType()); // Fraction diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 77cd5228..9ebd3f26 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -3,4 +3,4 @@ setlocale(LC_ALL, 'en_US.utf8'); // PHP 5.3 Compat -date_default_timezone_set('Europe/London'); +//date_default_timezone_set('Europe/London'); diff --git a/tests/data/Calculation/DateTime/DATEVALUE.php b/tests/data/Calculation/DateTime/DATEVALUE.php index 17110c74..0b0f110f 100644 --- a/tests/data/Calculation/DateTime/DATEVALUE.php +++ b/tests/data/Calculation/DateTime/DATEVALUE.php @@ -20,12 +20,12 @@ return [ '1900/2/28', ], [ - '#VALUE!', + '60', '29-02-1900', ], // MS Excel will fail with a #VALUE return, but PhpSpreadsheet can parse this date [ - '#VALUE!', + '60', '29th February 1900', ], [ @@ -159,30 +159,32 @@ return [ '#VALUE!', 'The 1st day of March 2007', ], - // 01/01 of the current year + // Jan 1 of the current year [ - 44197, + 'Y-01-01', '1 Jan', ], - // 31/12 of the current year + // Dec 31 of the current year [ - 44561, + 'Y-12-31', '31/12', ], - // Excel reads as 1st December 1931, not 31st December in current year + // Excel reads as 1st December 1931, not 31st December in current year. + // This result is locale-dependent in Excel, in a manner not + // supported by PhpSpreadsheet. [ 11658, '12/31', ], - // 05/07 of the current year + // July 5 of the current year [ - 44382, + 'Y-07-05', '5-JUL', ], - // 05/07 of the current year + // July 5 of the current year [ - 44382, - '5 Jul', + 'Y-07-05', + '5 July', ], [ 39783, @@ -216,6 +218,11 @@ return [ '#VALUE!', 12, ], + // implicit day of month is 1 + [ + 40210, + 'Feb-2010', + ], [ 40221, '12-Feb-2010', @@ -294,4 +301,16 @@ return [ '#VALUE!', 'ABCDEFGHIJKMNOPQRSTUVWXYZ', ], + [ + '#VALUE!', + '1999', + ], + ['#VALUE!', '32/32'], + ['#VALUE!', '1910-'], + ['#VALUE!', '10--'], + ['#VALUE!', '--10'], + ['#VALUE!', '--1910'], + //['#VALUE!', '-JUL-1910'], We can parse this, Excel can't + ['#VALUE!', '2008-08-'], + [36751, '0-08-13'], ]; diff --git a/tests/data/Calculation/DateTime/DAY.php b/tests/data/Calculation/DateTime/DAY.php index 8ba4ad41..81fab933 100644 --- a/tests/data/Calculation/DateTime/DAY.php +++ b/tests/data/Calculation/DateTime/DAY.php @@ -53,4 +53,19 @@ return [ 30, // Result for OpenOffice 0, ], + [ + 0, // Result for Excel + 0, // Result for OpenOffice + null, + ], + [ + 1, // Result for Excel + 1, // Result for OpenOffice + true, + ], + [ + 0, // Result for Excel + 0, // Result for OpenOffice + false, + ], ]; diff --git a/tests/data/Calculation/DateTime/ISOWEEKNUM.php b/tests/data/Calculation/DateTime/ISOWEEKNUM.php index 78a4d3e8..b6afd303 100644 --- a/tests/data/Calculation/DateTime/ISOWEEKNUM.php +++ b/tests/data/Calculation/DateTime/ISOWEEKNUM.php @@ -33,4 +33,14 @@ return [ '#VALUE!', '1800-01-01', ], + ['52', null], + ['53', '1904-01-01'], + ['52', '1900-01-01'], + ['1', '1900-01-07'], + ['1', '1900-01-08'], + ['2', '1900-01-09'], + ['9', '1900-03-04'], + ['10', '1900-03-05'], + ['#NUM!', '-1'], + [39, '1000'], ]; diff --git a/tests/data/Calculation/DateTime/NETWORKDAYS.php b/tests/data/Calculation/DateTime/NETWORKDAYS.php index d62e501c..db548ddc 100644 --- a/tests/data/Calculation/DateTime/NETWORKDAYS.php +++ b/tests/data/Calculation/DateTime/NETWORKDAYS.php @@ -100,4 +100,22 @@ return [ '31-Jan-2007', '1-Feb-2007', ], + ['#VALUE!', 'ABQZ', '1-Feb-2007'], + ['#VALUE!', '1-Feb-2007', 'ABQZ'], + [10, '2021-02-13', '2021-02-27'], + [10, '2021-02-14', '2021-02-27'], + [3, '2021-02-14', '2021-02-17'], + [8, '2021-02-14', '2021-02-24'], + [9, '2021-02-14', '2021-02-25'], + [10, '2021-02-14', '2021-02-26'], + [9, '2021-02-13', '2021-02-25'], + [10, '2021-02-12', '2021-02-25'], + [ + '#VALUE!', + '10-Jan-1961', + '19-Dec-1960', + '25-Dec-1960', + 'ABQZ', + '01-Jan-1961', + ], ]; diff --git a/tests/data/Calculation/DateTime/WEEKDAY.php b/tests/data/Calculation/DateTime/WEEKDAY.php index 8cc68082..fadf11f7 100644 --- a/tests/data/Calculation/DateTime/WEEKDAY.php +++ b/tests/data/Calculation/DateTime/WEEKDAY.php @@ -110,4 +110,11 @@ return [ '#NUM!', -1, ], + [1, null], + [1, false], + [2, true], + [1, '1900-01-01'], + [7, '1900-01-01', 2], + [7, null, 2], + [7, '1900-02-05', 2], ]; diff --git a/tests/data/Calculation/DateTime/WEEKNUM.php b/tests/data/Calculation/DateTime/WEEKNUM.php index d73ee463..b109a534 100644 --- a/tests/data/Calculation/DateTime/WEEKNUM.php +++ b/tests/data/Calculation/DateTime/WEEKNUM.php @@ -173,4 +173,31 @@ return [ 1, '2025-12-29', 21, ], + ['9', '1900-03-01'], + ['2', '1900-01-07', 2], + ['2', '1905-01-07', 2], + ['1', '1900-01-01'], + ['1', '1900-01-01', 2], + ['2', '1900-01-02', 2], + ['1', null, 11], + ['1', null, 12], + ['1', null, 13], + ['1', null, 14], + ['1', null, 15], + ['1', null, 16], + ['0', null, 17], + ['1', '1905-01-01', 17], + ['0', null], + ['1', null, 2], + ['1', '1906-01-01'], + ['#VALUE!', true], + ['#VALUE!', false, 21], + ['52', null, 21], + ['53', '1904-01-01', 21], + ['52', '1900-01-01', 21], + ['1', '1900-01-07', 21], + ['1', '1900-01-08', 21], + ['2', '1900-01-09', 21], + ['9', '1900-03-04', 21], + ['10', '1900-03-05', 21], ]; diff --git a/tests/data/Calculation/DateTime/WORKDAY.php b/tests/data/Calculation/DateTime/WORKDAY.php index 76c517f9..fc5f6483 100644 --- a/tests/data/Calculation/DateTime/WORKDAY.php +++ b/tests/data/Calculation/DateTime/WORKDAY.php @@ -89,4 +89,20 @@ return [ ], ], ], + [ + 44242, + '15-Feb-2021', + 0, + ], + [ + '#VALUE!', + '5-Apr-2012', + 3, + [ + [ + '6-Apr-2012', + 'ABQZ', + ], + ], + ], ]; diff --git a/tests/data/Calculation/DateTime/YEARFRAC.php b/tests/data/Calculation/DateTime/YEARFRAC.php index 3e76087c..abdb71d9 100644 --- a/tests/data/Calculation/DateTime/YEARFRAC.php +++ b/tests/data/Calculation/DateTime/YEARFRAC.php @@ -559,5 +559,6 @@ return [ '2025-05-28', 1, ], - + ['#VALUE!', '2023-04-27', 'ABQZ', 1], + ['#VALUE!', 'ABQZ', '2023-04-07', 1], ]; diff --git a/tests/data/Shared/Date/ExcelToTimestamp1904.php b/tests/data/Shared/Date/ExcelToTimestamp1904.php index e0f30754..1c013ab4 100644 --- a/tests/data/Shared/Date/ExcelToTimestamp1904.php +++ b/tests/data/Shared/Date/ExcelToTimestamp1904.php @@ -29,17 +29,17 @@ return [ ], // 06:00:00 [ - 21600, + gmmktime(6, 0, 0, 1, 1, 1904), // 32-bit safe - no Y2038 problem 0.25, ], // 08:00.00 [ - 28800, + gmmktime(8, 0, 0, 1, 1, 1904), // 32-bit safe - no Y2038 problem 0.3333333333333333333, ], - // 02:57:46 + // 13:02:13 [ - 46933, + gmmktime(13, 02, 13, 1, 1, 1904), // 32-bit safe - no Y2038 problem 0.54321, ], ]; From 761c84a94693993b3192aea62805c6b0bbd55900 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 27 Feb 2021 23:10:33 +0100 Subject: [PATCH 08/89] Avoid the performance/memory overheads of "clone on modify" of $args (#1884) * Avoid the performance/memory overheads of "clone on modify" of $args when building the condition set/database for AVERAGEIFS(), MAXIFS() and MINIFS() * Avoid the performance/memory overheads of "clone on modify" of $args when building the condition set/database for COUNTIFS() --- .../Calculation/Statistical/Conditional.php | 76 +++++++++++-------- 1 file changed, 45 insertions(+), 31 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php index 02bb0782..ae334011 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php @@ -67,8 +67,8 @@ class Conditional return self::AVERAGEIF($args[2], $args[1], $args[0]); } - $conditions = self::buildConditionSet(...$args); - $database = self::buildDatabase(...$args); + $conditions = self::buildConditionSetForRange(...$args); + $database = self::buildDatabaseWithRange(...$args); return DAverage::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); } @@ -122,19 +122,8 @@ class Conditional return self::COUNTIF(...$args); } - $conditions = $database = []; - $pairCount = 1; - while (count($args) > 0) { - $conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [array_pop($args)]); - $database[] = array_merge( - [sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], - Functions::flattenArray(array_pop($args)) - ); - ++$pairCount; - } - - $conditions = array_map(null, ...$conditions); - $database = array_map(null, ...$database); + $database = self::buildDatabase(...$args); + $conditions = self::buildConditionSet(...$args); return DCount::evaluate($database, null, $conditions); } @@ -157,8 +146,8 @@ class Conditional return 0.0; } - $conditions = self::buildConditionSet(...$args); - $database = self::buildDatabase(...$args); + $conditions = self::buildConditionSetForRange(...$args); + $database = self::buildDatabaseWithRange(...$args); return DMax::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); } @@ -181,23 +170,22 @@ class Conditional return 0.0; } - $conditions = self::buildConditionSet(...$args); - $database = self::buildDatabase(...$args); + $conditions = self::buildConditionSetForRange(...$args); + $database = self::buildDatabaseWithRange(...$args); return DMin::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); } private static function buildConditionSet(...$args): array { - array_shift($args); + $conditions = self::buildConditions(1, ...$args); - $conditions = []; - $pairCount = 1; - while (count($args) > 0) { - $conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [array_pop($args)]); - array_pop($args); - ++$pairCount; - } + return array_map(null, ...$conditions); + } + + private static function buildConditionSetForRange(...$args): array + { + $conditions = self::buildConditions(2, ...$args); if (count($conditions) === 1) { return array_map( @@ -211,20 +199,46 @@ class Conditional return array_map(null, ...$conditions); } + private static function buildConditions(int $startOffset, ...$args): array + { + $conditions = []; + + $pairCount = 1; + $argumentCount = count($args); + for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) { + $conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [$args[$argument]]); + ++$pairCount; + } + + return $conditions; + } + private static function buildDatabase(...$args): array + { + $database = []; + + return self::buildDataSet(0, $database, ...$args); + } + + private static function buildDatabaseWithRange(...$args): array { $database = []; $database[] = array_merge( [self::VALUE_COLUMN_NAME], - Functions::flattenArray(array_shift($args)) + Functions::flattenArray($args[0]) ); + return self::buildDataSet(1, $database, ...$args); + } + + private static function buildDataSet(int $startOffset, array $database, ...$args): array + { $pairCount = 1; - while (count($args) > 0) { - array_pop($args); + $argumentCount = count($args); + for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) { $database[] = array_merge( [sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], - Functions::flattenArray(array_pop($args)) + Functions::flattenArray($args[$argument]) ); ++$pairCount; } From ee969fdcfe6c031a487375da3206915486b3e468 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 28 Feb 2021 10:24:33 +0100 Subject: [PATCH 09/89] Additional conditionals from math trig (#1885) * Use our new Conditional logic to implement the SUMIF() and SUMIFS() Mathematical functions --- samples/Basic/40_Duplicate_style.php | 2 +- .../Calculation/Calculation.php | 4 +- .../Calculation/Database/DatabaseAbstract.php | 4 + src/PhpSpreadsheet/Calculation/MathTrig.php | 93 ++++-------------- .../Calculation/Statistical/Conditional.php | 98 +++++++++++++++---- tests/data/Calculation/MathTrig/SUMIF.php | 18 ++++ tests/data/Calculation/MathTrig/SUMIFS.php | 19 ++++ .../Calculation/Statistical/AVERAGEIFS.php | 3 + .../data/Calculation/Statistical/COUNTIFS.php | 3 + tests/data/Calculation/Statistical/MAXIFS.php | 3 + tests/data/Calculation/Statistical/MINIFS.php | 3 + 11 files changed, 151 insertions(+), 99 deletions(-) diff --git a/samples/Basic/40_Duplicate_style.php b/samples/Basic/40_Duplicate_style.php index 0366703d..38f7fb49 100644 --- a/samples/Basic/40_Duplicate_style.php +++ b/samples/Basic/40_Duplicate_style.php @@ -30,7 +30,7 @@ for ($col = 1; $col <= 50; ++$col) { } } $d = microtime(true) - $t; -$helper->log('Add data (end) . time: ' . round((string) ($d . 2)) . ' s'); +$helper->log('Add data (end) . time: ' . (string) round($d, 2) . ' s'); // Save $helper->write($spreadsheet, __FILE__); diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index e114a00d..a3f45f19 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2314,12 +2314,12 @@ class Calculation ], 'SUMIF' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUMIF'], + 'functionCall' => [Statistical\Conditional::class, 'SUMIF'], 'argumentCount' => '2,3', ], 'SUMIFS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUMIFS'], + 'functionCall' => [Statistical\Conditional::class, 'SUMIFS'], 'argumentCount' => '3+', ], 'SUMPRODUCT' => [ diff --git a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php index 2148ebc0..cf48bd88 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php +++ b/src/PhpSpreadsheet/Calculation/Database/DatabaseAbstract.php @@ -162,6 +162,10 @@ abstract class DatabaseAbstract $dataValue = ($dataValues[$key]) ? 'TRUE' : 'FALSE'; } elseif ($dataValues[$key] !== null) { $dataValue = $dataValues[$key]; + // escape quotes if we have a string containing quotes + if (is_string($dataValue) && strpos($dataValue, '"') !== false) { + $dataValue = str_replace('"', '""', $dataValue); + } $dataValue = (is_string($dataValue)) ? Calculation::wrapResult(strtoupper($dataValue)) : $dataValue; } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index f12cc9df..5a966cbf 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -1284,44 +1284,22 @@ class MathTrig * Totals the values of cells that contain numbers within the list of arguments * * Excel Function: - * SUMIF(value1[,value2[, ...]],condition) + * SUMIF(range, criteria, [sum_range]) * - * @param mixed $aArgs Data values - * @param string $condition the criteria that defines which cells will be summed - * @param mixed $sumArgs + * @Deprecated 1.17.0 + * + * @see Statistical\Conditional::SUMIF() + * Use the SUMIF() method in the Statistical\Conditional class instead + * + * @param mixed $range Data values + * @param string $criteria the criteria that defines which cells will be summed + * @param mixed $sumRange * * @return float */ - public static function SUMIF($aArgs, $condition, $sumArgs = []) + public static function SUMIF($range, $criteria, $sumRange = []) { - $returnValue = 0; - - $aArgs = Functions::flattenArray($aArgs); - $sumArgs = Functions::flattenArray($sumArgs); - if (empty($sumArgs)) { - $sumArgs = $aArgs; - } - $condition = Functions::ifCondition($condition); - // Loop through arguments - foreach ($aArgs as $key => $arg) { - if (!is_numeric($arg)) { - $arg = str_replace('"', '""', $arg); - $arg = Calculation::wrapResult(strtoupper($arg)); - } - - $testCondition = '=' . $arg . $condition; - $sumValue = array_key_exists($key, $sumArgs) ? $sumArgs[$key] : 0; - - if ( - is_numeric($sumValue) && - Calculation::getInstance()->_calculateFormulaValue($testCondition) - ) { - // Is it a value within our criteria and only numeric can be added to the result - $returnValue += $sumValue; - } - } - - return $returnValue; + return Statistical\Conditional::SUMIF($range, $criteria, $sumRange); } /** @@ -1330,7 +1308,12 @@ class MathTrig * Totals the values of cells that contain numbers within the list of arguments * * Excel Function: - * SUMIFS(value1[,value2[, ...]],condition) + * SUMIFS(sum_range, criteria_range1, criteria1, [criteria_range2, criteria2], ...) + * + * @Deprecated 1.17.0 + * + * @see Statistical\Conditional::SUMIFS() + * Use the SUMIFS() method in the Statistical\Conditional class instead * * @param mixed $args Data values * @@ -1338,47 +1321,7 @@ class MathTrig */ public static function SUMIFS(...$args) { - $arrayList = $args; - - // Return value - $returnValue = 0; - - $sumArgs = Functions::flattenArray(array_shift($arrayList)); - $aArgsArray = []; - $conditions = []; - - while (count($arrayList) > 0) { - $aArgsArray[] = Functions::flattenArray(array_shift($arrayList)); - $conditions[] = Functions::ifCondition(array_shift($arrayList)); - } - - // Loop through each sum and see if arguments and conditions are true - foreach ($sumArgs as $index => $value) { - $valid = true; - - foreach ($conditions as $cidx => $condition) { - $arg = $aArgsArray[$cidx][$index]; - - // Loop through arguments - if (!is_numeric($arg)) { - $arg = Calculation::wrapResult(strtoupper($arg)); - } - $testCondition = '=' . $arg . $condition; - if (!Calculation::getInstance()->_calculateFormulaValue($testCondition)) { - // Is not a value within our criteria - $valid = false; - - break; // if false found, don't need to check other conditions - } - } - - if ($valid) { - $returnValue += $value; - } - } - - // Return - return $returnValue; + return Statistical\Conditional::SUMIFS(...$args); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php index ae334011..7ed1e714 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php @@ -6,6 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Database\DAverage; use PhpOffice\PhpSpreadsheet\Calculation\Database\DCount; use PhpOffice\PhpSpreadsheet\Calculation\Database\DMax; use PhpOffice\PhpSpreadsheet\Calculation\Database\DMin; +use PhpOffice\PhpSpreadsheet\Calculation\Database\DSum; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Conditional @@ -30,18 +31,7 @@ class Conditional */ public static function AVERAGEIF($range, $condition, $averageRange = []) { - $range = Functions::flattenArray($range); - $averageRange = Functions::flattenArray($averageRange); - if (empty($averageRange)) { - $averageRange = $range; - } - - $database = array_map( - null, - array_merge([self::CONDITION_COLUMN_NAME], $range), - array_merge([self::VALUE_COLUMN_NAME], $averageRange) - ); - + $database = self::databaseFromRangeAndValue($range, $averageRange); $condition = [[self::CONDITION_COLUMN_NAME, self::VALUE_COLUMN_NAME], [$condition, null]]; return DAverage::evaluate($database, self::VALUE_COLUMN_NAME, $condition); @@ -64,11 +54,11 @@ class Conditional if (empty($args)) { return 0.0; } elseif (count($args) === 3) { - return self::AVERAGEIF($args[2], $args[1], $args[0]); + return self::AVERAGEIF($args[1], $args[2], $args[0]); } - $conditions = self::buildConditionSetForRange(...$args); - $database = self::buildDatabaseWithRange(...$args); + $conditions = self::buildConditionSetForValueRange(...$args); + $database = self::buildDatabaseWithValueRange(...$args); return DAverage::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); } @@ -146,8 +136,8 @@ class Conditional return 0.0; } - $conditions = self::buildConditionSetForRange(...$args); - $database = self::buildDatabaseWithRange(...$args); + $conditions = self::buildConditionSetForValueRange(...$args); + $database = self::buildDatabaseWithValueRange(...$args); return DMax::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); } @@ -170,12 +160,60 @@ class Conditional return 0.0; } - $conditions = self::buildConditionSetForRange(...$args); - $database = self::buildDatabaseWithRange(...$args); + $conditions = self::buildConditionSetForValueRange(...$args); + $database = self::buildDatabaseWithValueRange(...$args); return DMin::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); } + /** + * SUMIF. + * + * Totals the values of cells that contain numbers within the list of arguments + * + * Excel Function: + * SUMIF(range, criteria, [sum_range]) + * + * @param mixed $range Data values + * @param mixed $sumRange + * @param mixed $condition + * + * @return float + */ + public static function SUMIF($range, $condition, $sumRange = []) + { + $database = self::databaseFromRangeAndValue($range, $sumRange); + $condition = [[self::CONDITION_COLUMN_NAME, self::VALUE_COLUMN_NAME], [$condition, null]]; + + return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $condition); + } + + /** + * SUMIFS. + * + * Counts the number of cells that contain numbers within the list of arguments + * + * Excel Function: + * SUMIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2]…) + * + * @param mixed $args Pairs of Ranges and Criteria + * + * @return null|float|string + */ + public static function SUMIFS(...$args) + { + if (empty($args)) { + return 0.0; + } elseif (count($args) === 3) { + return self::SUMIF($args[1], $args[2], $args[0]); + } + + $conditions = self::buildConditionSetForValueRange(...$args); + $database = self::buildDatabaseWithValueRange(...$args); + + return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $conditions); + } + private static function buildConditionSet(...$args): array { $conditions = self::buildConditions(1, ...$args); @@ -183,7 +221,7 @@ class Conditional return array_map(null, ...$conditions); } - private static function buildConditionSetForRange(...$args): array + private static function buildConditionSetForValueRange(...$args): array { $conditions = self::buildConditions(2, ...$args); @@ -220,7 +258,7 @@ class Conditional return self::buildDataSet(0, $database, ...$args); } - private static function buildDatabaseWithRange(...$args): array + private static function buildDatabaseWithValueRange(...$args): array { $database = []; $database[] = array_merge( @@ -245,4 +283,22 @@ class Conditional return array_map(null, ...$database); } + + private static function databaseFromRangeAndValue(array $range, array $valueRange = []): array + { + $range = Functions::flattenArray($range); + + $valueRange = Functions::flattenArray($valueRange); + if (empty($valueRange)) { + $valueRange = $range; + } + + $database = array_map( + null, + array_merge([self::CONDITION_COLUMN_NAME], $range), + array_merge([self::VALUE_COLUMN_NAME], $valueRange) + ); + + return $database; + } } diff --git a/tests/data/Calculation/MathTrig/SUMIF.php b/tests/data/Calculation/MathTrig/SUMIF.php index 4bd7fc4d..9cba4d15 100644 --- a/tests/data/Calculation/MathTrig/SUMIF.php +++ b/tests/data/Calculation/MathTrig/SUMIF.php @@ -120,4 +120,22 @@ return [ [5], ], ], + [ + 157559, + ['Jan', 'Jan', 'Jan', 'Jan', 'Feb', 'Feb', 'Feb', 'Feb'], + 'Feb', + [36693, 22100, 53321, 34440, 29889, 50090, 32080, 45500], + ], + [ + 66582, + ['North 1', 'North 2', 'South 1', 'South 2', 'North 1', 'North 2', 'South 1', 'South 2,'], + 'North 1', + [36693, 22100, 53321, 34440, 29889, 50090, 32080, 45500], + ], + [ + 138772, + ['North 1', 'North 2', 'South 1', 'South 2', 'North 1', 'North 2', 'South 1', 'South 2,'], + 'North ?', + [36693, 22100, 53321, 34440, 29889, 50090, 32080, 45500], + ], ]; diff --git a/tests/data/Calculation/MathTrig/SUMIFS.php b/tests/data/Calculation/MathTrig/SUMIFS.php index a374dd14..9d860c30 100644 --- a/tests/data/Calculation/MathTrig/SUMIFS.php +++ b/tests/data/Calculation/MathTrig/SUMIFS.php @@ -1,6 +1,9 @@ 2', + ['Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol', 'Jeff', 'Chris', 'Carol'], + 'Jeff', + ], ]; diff --git a/tests/data/Calculation/Statistical/AVERAGEIFS.php b/tests/data/Calculation/Statistical/AVERAGEIFS.php index 6c94300c..d5bc6dad 100644 --- a/tests/data/Calculation/Statistical/AVERAGEIFS.php +++ b/tests/data/Calculation/Statistical/AVERAGEIFS.php @@ -1,6 +1,9 @@ Date: Sun, 28 Feb 2021 13:18:51 +0100 Subject: [PATCH 10/89] Initial Formula Translation tests (#1886) * Initial Formula Translation tests --- samples/Basic/43_Merge_workbooks.php | 4 ++ .../Calculation/TranslationTest.php | 51 +++++++++++++++++++ tests/data/Calculation/Translations.php | 48 +++++++++++++++++ 3 files changed, 103 insertions(+) create mode 100644 tests/PhpSpreadsheetTests/Calculation/TranslationTest.php create mode 100644 tests/data/Calculation/Translations.php diff --git a/samples/Basic/43_Merge_workbooks.php b/samples/Basic/43_Merge_workbooks.php index 86314b3b..28353cc6 100644 --- a/samples/Basic/43_Merge_workbooks.php +++ b/samples/Basic/43_Merge_workbooks.php @@ -18,6 +18,10 @@ $helper->logRead('Xlsx', $filename2, $callStartTime); foreach ($spreadsheet2->getSheetNames() as $sheetName) { $sheet = $spreadsheet2->getSheetByName($sheetName); + if ($sheet === null) { + continue; + } + $sheet->setTitle($sheet->getTitle() . ' copied'); $spreadsheet1->addExternalSheet($sheet); } diff --git a/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php new file mode 100644 index 00000000..1eb66a0a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/TranslationTest.php @@ -0,0 +1,51 @@ +compatibilityMode = Functions::getCompatibilityMode(); + $this->returnDate = Functions::getReturnDateType(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); + Functions::setReturnDateType($this->returnDate); + } + + /** + * @dataProvider providerTranslations + */ + public function testTranslation(string $expectedResult, string $locale, string $formula): void + { + $validLocale = Settings::setLocale($locale); + if (!$validLocale) { + self::markTestSkipped("Unable to set locale to {$locale}"); + } + + $translatedFormula = Calculation::getInstance()->_translateFormulaToLocale($formula); + self::assertSame($expectedResult, $translatedFormula); + + $restoredFormula = Calculation::getInstance()->_translateFormulaToEnglish($translatedFormula); + self::assertSame($formula, $restoredFormula); + } + + public function providerTranslations() + { + return require 'tests/data/Calculation/Translations.php'; + } +} diff --git a/tests/data/Calculation/Translations.php b/tests/data/Calculation/Translations.php new file mode 100644 index 00000000..d470a05c --- /dev/null +++ b/tests/data/Calculation/Translations.php @@ -0,0 +1,48 @@ + Date: Sun, 28 Feb 2021 09:58:57 -0700 Subject: [PATCH 11/89] Pdf Writer strtoupper() fix (#1629) * _setPageSize's strtoupper() on array argument PhpSpreadsheet/Writer/Pdf.php Class defines a protected static mixed array called $paperSizes, this array contains string values along with array values. 'strtoupper() expects parameter 1 to be string, array given' error happens due to array passed to $paperSize variable from that $paperSizes mixed array on the Mpdf Class where Pdf extends Examples of cases, when a 'Letter' paper size is chosen, then no problem occurs since the index in that value for the array is a string value, but when 'Tabloid' paper size is chosen the value in the index for that paper size is an array, that's when the strtoupper() error happens * _setPageSize's strtoupper() on array argument PhpSpreadsheet/Writer/Pdf.php Class defines a protected static mixed array called $paperSizes, this array contains string values along with array values. 'strtoupper() expects parameter 1 to be string, array given' error happens due to array passed to $paperSize variable from that $paperSizes mixed array on the Dompdf Class where Pdf extends Examples of cases; when a 'Letter' paper size is chosen, then no problem occurs since the index in the array for that value a string, but when 'Tabloid' paper size is chosen the value in the index for that paper size is an array, that's when the strtoupper() error happens. --- src/PhpSpreadsheet/Writer/Pdf/Dompdf.php | 2 +- src/PhpSpreadsheet/Writer/Pdf/Mpdf.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php index 9ae2ccee..87e8eeb5 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Dompdf.php @@ -59,7 +59,7 @@ class Dompdf extends Pdf // Create PDF $pdf = $this->createExternalWriterInstance(); - $pdf->setPaper(strtolower($paperSize), $orientation); + $pdf->setPaper($paperSize, $orientation); $pdf->loadHtml($this->generateHTMLAll()); $pdf->render(); diff --git a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php index 75e0010d..56ac6930 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Mpdf.php @@ -64,7 +64,7 @@ class Mpdf extends Pdf $config = ['tempDir' => $this->tempDir . '/mpdf']; $pdf = $this->createExternalWriterInstance($config); $ortmp = $orientation; - $pdf->_setPageSize(strtoupper($paperSize), $ortmp); + $pdf->_setPageSize($paperSize, $ortmp); $pdf->DefOrientation = $orientation; $pdf->AddPageByArray([ 'orientation' => $orientation, From 8721f795fc4537b2bdf9daf93e3b52c4805a214d Mon Sep 17 00:00:00 2001 From: Ivan Stanojevic Date: Mon, 1 Mar 2021 12:33:35 +0100 Subject: [PATCH 12/89] Update ReferenceHelper.php (#1873) * Update ReferenceHelper for Defined Names --- src/PhpSpreadsheet/ReferenceHelper.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 13f7cf71..513f0a53 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -608,7 +608,7 @@ class ReferenceHelper // Update workbook: define names if (count($pSheet->getParent()->getDefinedNames()) > 0) { foreach ($pSheet->getParent()->getDefinedNames() as $definedName) { - if ($definedName->getWorksheet()->getHashCode() === $pSheet->getHashCode()) { + if ($definedName->getWorksheet() !== null && $definedName->getWorksheet()->getHashCode() === $pSheet->getHashCode()) { $definedName->setValue($this->updateCellReference($definedName->getValue(), $pBefore, $pNumCols, $pNumRows)); } } From 2eaf9b53aa0625d4158f9a125a03fd16a01fce1a Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 2 Mar 2021 09:07:28 +0100 Subject: [PATCH 13/89] Start splitting some of the basic Statistical functions out into separate classes (#1888) * Start splitting some of the basic Statistical functions out into separate classes containing just a few similar functions * Splitting some of the basic Statistical functions out into separate classes containing just a few similar functions - MAX(), MAXA(), MIN() and MINA() * Splitting some more of the basic Statistical functions out into separate classes containing just a few similar functions - StandardDeviations and Variances --- .../Calculation/Calculation.php | 40 +- .../Calculation/Database/DAverage.php | 4 +- .../Calculation/Database/DCount.php | 4 +- .../Calculation/Database/DCountA.php | 4 +- .../Calculation/Database/DMax.php | 4 +- .../Calculation/Database/DMin.php | 4 +- .../Calculation/Database/DStDev.php | 4 +- .../Calculation/Database/DStDevP.php | 4 +- .../Calculation/Database/DVar.php | 4 +- .../Calculation/Database/DVarP.php | 4 +- src/PhpSpreadsheet/Calculation/MathTrig.php | 18 +- .../Calculation/Statistical.php | 665 ++++-------------- .../Calculation/Statistical/AggregateBase.php | 50 ++ .../Calculation/Statistical/Averages.php | 137 ++++ .../Calculation/Statistical/Counts.php | 95 +++ .../Calculation/Statistical/MaxMinBase.php | 17 + .../Calculation/Statistical/Maximum.php | 78 ++ .../Calculation/Statistical/Minimum.php | 78 ++ .../Statistical/StandardDeviations.php | 181 +++++ .../Calculation/Statistical/VarianceBase.php | 26 + .../Calculation/Statistical/Variances.php | 183 +++++ src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php | 2 +- .../Functions/Statistical/StDevATest.php | 26 + .../Functions/Statistical/StDevPATest.php | 26 + .../Functions/Statistical/StDevPTest.php | 26 + .../Functions/Statistical/StDevTest.php | 26 + .../Functions/Statistical/VarATest.php | 26 + .../Functions/Statistical/VarPATest.php | 26 + .../Functions/Statistical/VarPTest.php | 26 + .../Functions/Statistical/VarTest.php | 26 + tests/data/Calculation/Statistical/STDEV.php | 12 + tests/data/Calculation/Statistical/STDEVA.php | 12 + tests/data/Calculation/Statistical/STDEVP.php | 12 + .../data/Calculation/Statistical/STDEVPA.php | 12 + tests/data/Calculation/Statistical/VAR.php | 8 + tests/data/Calculation/Statistical/VARA.php | 8 + tests/data/Calculation/Statistical/VARP.php | 8 + tests/data/Calculation/Statistical/VARPA.php | 8 + 38 files changed, 1314 insertions(+), 580 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/AggregateBase.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Averages.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Counts.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Maximum.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Minimum.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Variances.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPATest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarATest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPATest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarTest.php create mode 100644 tests/data/Calculation/Statistical/STDEV.php create mode 100644 tests/data/Calculation/Statistical/STDEVA.php create mode 100644 tests/data/Calculation/Statistical/STDEVP.php create mode 100644 tests/data/Calculation/Statistical/STDEVPA.php create mode 100644 tests/data/Calculation/Statistical/VAR.php create mode 100644 tests/data/Calculation/Statistical/VARA.php create mode 100644 tests/data/Calculation/Statistical/VARP.php create mode 100644 tests/data/Calculation/Statistical/VARPA.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index a3f45f19..bc0baa4d 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -328,17 +328,17 @@ class Calculation ], 'AVEDEV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'AVEDEV'], + 'functionCall' => [Statistical\Averages::class, 'AVEDEV'], 'argumentCount' => '1+', ], 'AVERAGE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'AVERAGE'], + 'functionCall' => [Statistical\Averages::class, 'AVERAGE'], 'argumentCount' => '1+', ], 'AVERAGEA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'AVERAGEA'], + 'functionCall' => [Statistical\Averages::class, 'AVERAGEA'], 'argumentCount' => '1+', ], 'AVERAGEIF' => [ @@ -624,17 +624,17 @@ class Calculation ], 'COUNT' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'COUNT'], + 'functionCall' => [Statistical\Counts::class, 'COUNT'], 'argumentCount' => '1+', ], 'COUNTA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'COUNTA'], + 'functionCall' => [Statistical\Counts::class, 'COUNTA'], 'argumentCount' => '1+', ], 'COUNTBLANK' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'COUNTBLANK'], + 'functionCall' => [Statistical\Counts::class, 'COUNTBLANK'], 'argumentCount' => '1', ], 'COUNTIF' => [ @@ -1620,12 +1620,12 @@ class Calculation ], 'MAX' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MAX'], + 'functionCall' => [Statistical\Maximum::class, 'MAX'], 'argumentCount' => '1+', ], 'MAXA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MAXA'], + 'functionCall' => [Statistical\Maximum::class, 'MAXA'], 'argumentCount' => '1+', ], 'MAXIFS' => [ @@ -1665,12 +1665,12 @@ class Calculation ], 'MIN' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MIN'], + 'functionCall' => [Statistical\Minimum::class, 'MIN'], 'argumentCount' => '1+', ], 'MINA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MINA'], + 'functionCall' => [Statistical\Minimum::class, 'MINA'], 'argumentCount' => '1+', ], 'MINIFS' => [ @@ -2263,22 +2263,22 @@ class Calculation ], 'STDEV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'STDEV'], + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'], 'argumentCount' => '1+', ], 'STDEV.S' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'STDEV'], + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEV'], 'argumentCount' => '1+', ], 'STDEV.P' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'STDEVP'], + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVP'], 'argumentCount' => '1+', ], 'STDEVA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'STDEVA'], + 'functionCall' => [Statistical\StandardDeviations::class, 'STDEVA'], 'argumentCount' => '1+', ], 'STDEVP' => [ @@ -2524,32 +2524,32 @@ class Calculation ], 'VAR' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'VARFunc'], + 'functionCall' => [Statistical\Variances::class, 'VAR'], 'argumentCount' => '1+', ], 'VAR.P' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'VARP'], + 'functionCall' => [Statistical\Variances::class, 'VARP'], 'argumentCount' => '1+', ], 'VAR.S' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'VARFunc'], + 'functionCall' => [Statistical\Variances::class, 'VAR'], 'argumentCount' => '1+', ], 'VARA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'VARA'], + 'functionCall' => [Statistical\Variances::class, 'VARA'], 'argumentCount' => '1+', ], 'VARP' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'VARP'], + 'functionCall' => [Statistical\Variances::class, 'VARP'], 'argumentCount' => '1+', ], 'VARPA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'VARPA'], + 'functionCall' => [Statistical\Variances::class, 'VARPA'], 'argumentCount' => '1+', ], 'VDB' => [ diff --git a/src/PhpSpreadsheet/Calculation/Database/DAverage.php b/src/PhpSpreadsheet/Calculation/Database/DAverage.php index ea45beda..738cb78e 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DAverage.php +++ b/src/PhpSpreadsheet/Calculation/Database/DAverage.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; class DAverage extends DatabaseAbstract { @@ -38,7 +38,7 @@ class DAverage extends DatabaseAbstract return null; } - return Statistical::AVERAGE( + return Averages::AVERAGE( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DCount.php b/src/PhpSpreadsheet/Calculation/Database/DCount.php index da173c8c..bf41d6b5 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DCount.php +++ b/src/PhpSpreadsheet/Calculation/Database/DCount.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts; class DCount extends DatabaseAbstract { @@ -36,7 +36,7 @@ class DCount extends DatabaseAbstract { $field = self::fieldExtract($database, $field); - return Statistical::COUNT( + return Counts::COUNT( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DCountA.php b/src/PhpSpreadsheet/Calculation/Database/DCountA.php index 1beaf012..c48e53c5 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DCountA.php +++ b/src/PhpSpreadsheet/Calculation/Database/DCountA.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts; class DCountA extends DatabaseAbstract { @@ -35,7 +35,7 @@ class DCountA extends DatabaseAbstract { $field = self::fieldExtract($database, $field); - return Statistical::COUNTA( + return Counts::COUNTA( self::getFilteredColumn($database, $field ?? 0, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DMax.php b/src/PhpSpreadsheet/Calculation/Database/DMax.php index 8918c54d..6cf2f20d 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DMax.php +++ b/src/PhpSpreadsheet/Calculation/Database/DMax.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Maximum; class DMax extends DatabaseAbstract { @@ -39,7 +39,7 @@ class DMax extends DatabaseAbstract return null; } - return Statistical::MAX( + return Maximum::MAX( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DMin.php b/src/PhpSpreadsheet/Calculation/Database/DMin.php index 357c9a91..5668bcf6 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DMin.php +++ b/src/PhpSpreadsheet/Calculation/Database/DMin.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Minimum; class DMin extends DatabaseAbstract { @@ -39,7 +39,7 @@ class DMin extends DatabaseAbstract return null; } - return Statistical::MIN( + return Minimum::MIN( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DStDev.php b/src/PhpSpreadsheet/Calculation/Database/DStDev.php index 58bb4f7d..cfc7e952 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DStDev.php +++ b/src/PhpSpreadsheet/Calculation/Database/DStDev.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; class DStDev extends DatabaseAbstract { @@ -39,7 +39,7 @@ class DStDev extends DatabaseAbstract return null; } - return Statistical::STDEV( + return StandardDeviations::STDEV( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DStDevP.php b/src/PhpSpreadsheet/Calculation/Database/DStDevP.php index 0d2e050d..2a04c5d9 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DStDevP.php +++ b/src/PhpSpreadsheet/Calculation/Database/DStDevP.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; class DStDevP extends DatabaseAbstract { @@ -39,7 +39,7 @@ class DStDevP extends DatabaseAbstract return null; } - return Statistical::STDEVP( + return StandardDeviations::STDEVP( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DVar.php b/src/PhpSpreadsheet/Calculation/Database/DVar.php index 62c9f6c6..c70da073 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DVar.php +++ b/src/PhpSpreadsheet/Calculation/Database/DVar.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances; class DVar extends DatabaseAbstract { @@ -39,7 +39,7 @@ class DVar extends DatabaseAbstract return null; } - return Statistical::VARFunc( + return Variances::VAR( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DVarP.php b/src/PhpSpreadsheet/Calculation/Database/DVarP.php index 3a9744cb..f22f2cca 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DVarP.php +++ b/src/PhpSpreadsheet/Calculation/Database/DVarP.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Database; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances; class DVarP extends DatabaseAbstract { @@ -39,7 +39,7 @@ class DVarP extends DatabaseAbstract return null; } - return Statistical::VARP( + return Variances::VARP( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index 5a966cbf..e72e24cd 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -1222,27 +1222,27 @@ class MathTrig $aArgs = self::filterFormulaArgs($cellReference, $aArgs); switch ($subtotal) { case 1: - return Statistical::AVERAGE($aArgs); + return Statistical\Averages::AVERAGE($aArgs); case 2: - return Statistical::COUNT($aArgs); + return Statistical\Counts::COUNT($aArgs); case 3: - return Statistical::COUNTA($aArgs); + return Statistical\Counts::COUNTA($aArgs); case 4: - return Statistical::MAX($aArgs); + return Statistical\Maximum::MAX($aArgs); case 5: - return Statistical::MIN($aArgs); + return Statistical\Minimum::MIN($aArgs); case 6: return self::PRODUCT($aArgs); case 7: - return Statistical::STDEV($aArgs); + return Statistical\StandardDeviations::STDEV($aArgs); case 8: - return Statistical::STDEVP($aArgs); + return Statistical\StandardDeviations::STDEVP($aArgs); case 9: return self::SUM($aArgs); case 10: - return Statistical::VARFunc($aArgs); + return Statistical\Variances::VAR($aArgs); case 11: - return Statistical::VARP($aArgs); + return Statistical\Variances::VARP($aArgs); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 2f695d5b..dc9c5b44 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -2,8 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Conditional; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Maximum; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Minimum; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Permutations; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances; use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend; class Statistical @@ -520,48 +526,6 @@ class Statistical return Functions::NULL(); } - /** - * MS Excel does not count Booleans if passed as cell values, but they are counted if passed as literals. - * OpenOffice Calc always counts Booleans. - * Gnumeric never counts Booleans. - * - * @param mixed $arg - * @param mixed $k - * - * @return int|mixed - */ - private static function testAcceptedBoolean($arg, $k) - { - if ( - (is_bool($arg)) && - ((!Functions::isCellValue($k) && (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_EXCEL)) || - (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE)) - ) { - $arg = (int) $arg; - } - - return $arg; - } - - /** - * @param mixed $arg - * @param mixed $k - * - * @return bool - */ - private static function isAcceptedCountable($arg, $k) - { - if ( - ((is_numeric($arg)) && (!is_string($arg))) || - ((is_numeric($arg)) && (!Functions::isCellValue($k)) && - (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_GNUMERIC)) - ) { - return true; - } - - return false; - } - /** * AVEDEV. * @@ -571,45 +535,18 @@ class Statistical * Excel Function: * AVEDEV(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Averages::AVEDEV() + * Use the AVEDEV() method in the Statistical\Averages class instead + * * @param mixed ...$args Data values * * @return float|string */ public static function AVEDEV(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - // Return value - $returnValue = 0; - - $aMean = self::AVERAGE(...$args); - if ($aMean === Functions::DIV0()) { - return Functions::NAN(); - } elseif ($aMean === Functions::VALUE()) { - return Functions::VALUE(); - } - - $aCount = 0; - foreach ($aArgs as $k => $arg) { - $arg = self::testAcceptedBoolean($arg, $k); - // Is it a numeric value? - // 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(); - } - if (self::isAcceptedCountable($arg, $k)) { - $returnValue += abs($arg - $aMean); - ++$aCount; - } - } - - // Return - if ($aCount === 0) { - return Functions::DIV0(); - } - - return $returnValue / $aCount; + return Averages::AVEDEV(...$args); } /** @@ -620,35 +557,18 @@ class Statistical * Excel Function: * AVERAGE(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Averages::AVERAGE() + * Use the AVERAGE() method in the Statistical\Averages class instead + * * @param mixed ...$args Data values * * @return float|string */ public static function AVERAGE(...$args) { - $returnValue = $aCount = 0; - - // Loop through arguments - foreach (Functions::flattenArrayIndexed($args) as $k => $arg) { - $arg = self::testAcceptedBoolean($arg, $k); - // Is it a numeric value? - // 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(); - } - if (self::isAcceptedCountable($arg, $k)) { - $returnValue += $arg; - ++$aCount; - } - } - - // Return - if ($aCount > 0) { - return $returnValue / $aCount; - } - - return Functions::DIV0(); + return Averages::AVERAGE(...$args); } /** @@ -659,39 +579,18 @@ class Statistical * Excel Function: * AVERAGEA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Averages::AVERAGEA() + * Use the AVERAGEA() method in the Statistical\Averages class instead + * * @param mixed ...$args Data values * * @return float|string */ public static function AVERAGEA(...$args) { - $returnValue = null; - - $aCount = 0; - // Loop through arguments - foreach (Functions::flattenArrayIndexed($args) as $k => $arg) { - if ( - (is_bool($arg)) && - (!Functions::isMatrixValue($k)) - ) { - } else { - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { - if (is_bool($arg)) { - $arg = (int) $arg; - } elseif (is_string($arg)) { - $arg = 0; - } - $returnValue += $arg; - ++$aCount; - } - } - } - - if ($aCount > 0) { - return $returnValue / $aCount; - } - - return Functions::DIV0(); + return Averages::AVERAGEA(...$args); } /** @@ -715,7 +614,7 @@ class Statistical */ public static function AVERAGEIF($range, $condition, $averageRange = []) { - return Statistical\Conditional::AVERAGEIF($range, $condition, $averageRange); + return Conditional::AVERAGEIF($range, $condition, $averageRange); } /** @@ -1026,27 +925,18 @@ class Statistical * Excel Function: * COUNT(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Counts::COUNT() + * Use the COUNT() method in the Statistical\Counts class instead + * * @param mixed ...$args Data values * * @return int */ public static function COUNT(...$args) { - $returnValue = 0; - - // Loop through arguments - $aArgs = Functions::flattenArrayIndexed($args); - foreach ($aArgs as $k => $arg) { - $arg = self::testAcceptedBoolean($arg, $k); - // Is it a numeric value? - // 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 (self::isAcceptedCountable($arg, $k)) { - ++$returnValue; - } - } - - return $returnValue; + return Counts::COUNT(...$args); } /** @@ -1057,24 +947,18 @@ class Statistical * Excel Function: * COUNTA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Counts::COUNTA() + * Use the COUNTA() method in the Statistical\Counts class instead + * * @param mixed ...$args Data values * * @return int */ public static function COUNTA(...$args) { - $returnValue = 0; - - // Loop through arguments - $aArgs = Functions::flattenArrayIndexed($args); - foreach ($aArgs as $k => $arg) { - // Nulls are counted if literals, but not if cell values - if ($arg !== null || (!Functions::isCellValue($k))) { - ++$returnValue; - } - } - - return $returnValue; + return Counts::COUNTA(...$args); } /** @@ -1085,24 +969,18 @@ class Statistical * Excel Function: * COUNTBLANK(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Counts::COUNTBLANK() + * Use the COUNTBLANK() method in the Statistical\Counts class instead + * * @param mixed ...$args Data values * * @return int */ public static function COUNTBLANK(...$args) { - $returnValue = 0; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $arg) { - // Is it a blank cell? - if (($arg === null) || ((is_string($arg)) && ($arg == ''))) { - ++$returnValue; - } - } - - return $returnValue; + return Counts::COUNTBLANK(...$args); } /** @@ -1125,7 +1003,7 @@ class Statistical */ public static function COUNTIF($range, $condition) { - return Statistical\Conditional::COUNTIF($range, $condition); + return Conditional::COUNTIF($range, $condition); } /** @@ -1147,7 +1025,7 @@ class Statistical */ public static function COUNTIFS(...$args) { - return Statistical\Conditional::COUNTIFS(...$args); + return Conditional::COUNTIFS(...$args); } /** @@ -1325,7 +1203,7 @@ class Statistical // Return value $returnValue = null; - $aMean = self::AVERAGE($aArgs); + $aMean = Averages::AVERAGE($aArgs); if ($aMean != Functions::DIV0()) { $aCount = -1; foreach ($aArgs as $k => $arg) { @@ -1711,8 +1589,8 @@ class Statistical $aMean = MathTrig::PRODUCT($aArgs); if (is_numeric($aMean) && ($aMean > 0)) { - $aCount = self::COUNT($aArgs); - if (self::MIN($aArgs) > 0) { + $aCount = Counts::COUNT($aArgs); + if (Minimum::MIN($aArgs) > 0) { return $aMean ** (1 / $aCount); } } @@ -1772,7 +1650,7 @@ class Statistical // Loop through arguments $aArgs = Functions::flattenArray($args); - if (self::MIN($aArgs) < 0) { + if (Minimum::MIN($aArgs) < 0) { return Functions::NAN(); } $aCount = 0; @@ -1883,8 +1761,8 @@ class Statistical public static function KURT(...$args) { $aArgs = Functions::flattenArrayIndexed($args); - $mean = self::AVERAGE($aArgs); - $stdDev = self::STDEV($aArgs); + $mean = Averages::AVERAGE($aArgs); + $stdDev = StandardDeviations::STDEV($aArgs); if ($stdDev > 0) { $count = $summer = 0; @@ -1941,7 +1819,7 @@ class Statistical $mArgs[] = $arg; } } - $count = self::COUNT($mArgs); + $count = Counts::COUNT($mArgs); --$entry; if (($entry < 0) || ($entry >= $count) || ($count == 0)) { return Functions::NAN(); @@ -2184,30 +2062,18 @@ class Statistical * Excel Function: * MAX(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Maximum::MAX() + * Use the MAX() method in the Statistical\Maximum class instead + * * @param mixed ...$args Data values * * @return float */ public static function MAX(...$args) { - $returnValue = null; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - if (($returnValue === null) || ($arg > $returnValue)) { - $returnValue = $arg; - } - } - } - - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return Maximum::MAX(...$args); } /** @@ -2218,35 +2084,18 @@ class Statistical * Excel Function: * MAXA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Maximum::MAXA() + * Use the MAXA() method in the Statistical\Maximum class instead + * * @param mixed ...$args Data values * * @return float */ public static function MAXA(...$args) { - $returnValue = null; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { - if (is_bool($arg)) { - $arg = (int) $arg; - } elseif (is_string($arg)) { - $arg = 0; - } - if (($returnValue === null) || ($arg > $returnValue)) { - $returnValue = $arg; - } - } - } - - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return Maximum::MAXA(...$args); } /** @@ -2321,30 +2170,18 @@ class Statistical * Excel Function: * MIN(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Minimum::MIN() + * Use the MIN() method in the Statistical\Minimum class instead + * * @param mixed ...$args Data values * * @return float */ public static function MIN(...$args) { - $returnValue = null; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - if (($returnValue === null) || ($arg < $returnValue)) { - $returnValue = $arg; - } - } - } - - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return Minimum::MIN(...$args); } /** @@ -2355,35 +2192,18 @@ class Statistical * Excel Function: * MINA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Minimum::MINA() + * Use the MINA() method in the Statistical\Minimum class instead + * * @param mixed ...$args Data values * * @return float */ public static function MINA(...$args) { - $returnValue = null; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { - if (is_bool($arg)) { - $arg = (int) $arg; - } elseif (is_string($arg)) { - $arg = 0; - } - if (($returnValue === null) || ($arg < $returnValue)) { - $returnValue = $arg; - } - } - } - - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return Minimum::MINA(...$args); } /** @@ -2688,7 +2508,7 @@ class Statistical $mValueCount = count($mArgs); if ($mValueCount > 0) { sort($mArgs); - $count = self::COUNT($mArgs); + $count = Counts::COUNT($mArgs); $index = $entry * ($count - 1); $iBase = floor($index); if ($index == $iBase) { @@ -2775,7 +2595,7 @@ class Statistical */ public static function PERMUT($numObjs, $numInSet) { - return Statistical\Permutations::PERMUT($numObjs, $numInSet); + return Permutations::PERMUT($numObjs, $numInSet); } /** @@ -2930,8 +2750,8 @@ class Statistical public static function SKEW(...$args) { $aArgs = Functions::flattenArrayIndexed($args); - $mean = self::AVERAGE($aArgs); - $stdDev = self::STDEV($aArgs); + $mean = Averages::AVERAGE($aArgs); + $stdDev = StandardDeviations::STDEV($aArgs); $count = $summer = 0; // Loop through arguments @@ -3015,7 +2835,7 @@ class Statistical $mArgs[] = $arg; } } - $count = self::COUNT($mArgs); + $count = Counts::COUNT($mArgs); --$entry; if (($entry < 0) || ($entry >= $count) || ($count == 0)) { return Functions::NAN(); @@ -3065,45 +2885,18 @@ class Statistical * Excel Function: * STDEV(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\StandardDeviations::STDEV() + * Use the STDEV() method in the Statistical\StandardDeviations class instead + * * @param mixed ...$args Data values * * @return float|string The result, or a string containing an error */ public static function STDEV(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - // Return value - $returnValue = null; - - $aMean = self::AVERAGE($aArgs); - if ($aMean !== null) { - $aCount = -1; - foreach ($aArgs as $k => $arg) { - if ( - (is_bool($arg)) && - ((!Functions::isCellValue($k)) || (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) - ) { - $arg = (int) $arg; - } - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - if ($returnValue === null) { - $returnValue = ($arg - $aMean) ** 2; - } else { - $returnValue += ($arg - $aMean) ** 2; - } - ++$aCount; - } - } - - // Return - if (($aCount > 0) && ($returnValue >= 0)) { - return sqrt($returnValue / $aCount); - } - } - - return Functions::DIV0(); + return StandardDeviations::STDEV(...$args); } /** @@ -3114,48 +2907,18 @@ class Statistical * Excel Function: * STDEVA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\StandardDeviations::STDEVA() + * Use the STDEVA() method in the Statistical\StandardDeviations class instead + * * @param mixed ...$args Data values * * @return float|string */ public static function STDEVA(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - $returnValue = null; - - $aMean = self::AVERAGEA($aArgs); - if ($aMean !== null) { - $aCount = -1; - foreach ($aArgs as $k => $arg) { - if ( - (is_bool($arg)) && - (!Functions::isMatrixValue($k)) - ) { - } else { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { - if (is_bool($arg)) { - $arg = (int) $arg; - } elseif (is_string($arg)) { - $arg = 0; - } - if ($returnValue === null) { - $returnValue = ($arg - $aMean) ** 2; - } else { - $returnValue += ($arg - $aMean) ** 2; - } - ++$aCount; - } - } - } - - if (($aCount > 0) && ($returnValue >= 0)) { - return sqrt($returnValue / $aCount); - } - } - - return Functions::DIV0(); + return StandardDeviations::STDEVA(...$args); } /** @@ -3166,43 +2929,18 @@ class Statistical * Excel Function: * STDEVP(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\StandardDeviations::STDEVP() + * Use the STDEVP() method in the Statistical\StandardDeviations class instead + * * @param mixed ...$args Data values * * @return float|string */ public static function STDEVP(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - $returnValue = null; - - $aMean = self::AVERAGE($aArgs); - if ($aMean !== null) { - $aCount = 0; - foreach ($aArgs as $k => $arg) { - if ( - (is_bool($arg)) && - ((!Functions::isCellValue($k)) || (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) - ) { - $arg = (int) $arg; - } - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - if ($returnValue === null) { - $returnValue = ($arg - $aMean) ** 2; - } else { - $returnValue += ($arg - $aMean) ** 2; - } - ++$aCount; - } - } - - if (($aCount > 0) && ($returnValue >= 0)) { - return sqrt($returnValue / $aCount); - } - } - - return Functions::DIV0(); + return StandardDeviations::STDEVP(...$args); } /** @@ -3213,48 +2951,18 @@ class Statistical * Excel Function: * STDEVPA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\StandardDeviations::STDEVPA() + * Use the STDEVPA() method in the Statistical\StandardDeviations class instead + * * @param mixed ...$args Data values * * @return float|string */ public static function STDEVPA(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - $returnValue = null; - - $aMean = self::AVERAGEA($aArgs); - if ($aMean !== null) { - $aCount = 0; - foreach ($aArgs as $k => $arg) { - if ( - (is_bool($arg)) && - (!Functions::isMatrixValue($k)) - ) { - } else { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { - if (is_bool($arg)) { - $arg = (int) $arg; - } elseif (is_string($arg)) { - $arg = 0; - } - if ($returnValue === null) { - $returnValue = ($arg - $aMean) ** 2; - } else { - $returnValue += ($arg - $aMean) ** 2; - } - ++$aCount; - } - } - } - - if (($aCount > 0) && ($returnValue >= 0)) { - return sqrt($returnValue / $aCount); - } - } - - return Functions::DIV0(); + return StandardDeviations::STDEVPA(...$args); } /** @@ -3472,14 +3180,14 @@ class Statistical $mArgs[] = $arg; } } - $discard = floor(self::COUNT($mArgs) * $percent / 2); + $discard = floor(Counts::COUNT($mArgs) * $percent / 2); sort($mArgs); for ($i = 0; $i < $discard; ++$i) { array_pop($mArgs); array_shift($mArgs); } - return self::AVERAGE($mArgs); + return Averages::AVERAGE($mArgs); } return Functions::VALUE(); @@ -3493,38 +3201,18 @@ class Statistical * Excel Function: * VAR(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * * @param mixed ...$args Data values * * @return float|string (string if result is an error) + * + *@see Statistical\Variances::VAR() + * Use the VAR() method in the Statistical\Variances class instead */ public static function VARFunc(...$args) { - $returnValue = Functions::DIV0(); - - $summerA = $summerB = 0; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - $aCount = 0; - foreach ($aArgs as $arg) { - if (is_bool($arg)) { - $arg = (int) $arg; - } - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $summerA += ($arg * $arg); - $summerB += $arg; - ++$aCount; - } - } - - if ($aCount > 1) { - $summerA *= $aCount; - $summerB *= $summerB; - $returnValue = ($summerA - $summerB) / ($aCount * ($aCount - 1)); - } - - return $returnValue; + return Variances::VAR(...$args); } /** @@ -3535,51 +3223,18 @@ class Statistical * Excel Function: * VARA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Variances::VARA() + * Use the VARA() method in the Statistical\Variances class instead + * * @param mixed ...$args Data values * * @return float|string (string if result is an error) */ public static function VARA(...$args) { - $returnValue = Functions::DIV0(); - - $summerA = $summerB = 0; - - // Loop through arguments - $aArgs = Functions::flattenArrayIndexed($args); - $aCount = 0; - foreach ($aArgs as $k => $arg) { - if ( - (is_string($arg)) && - (Functions::isValue($k)) - ) { - return Functions::VALUE(); - } elseif ( - (is_string($arg)) && - (!Functions::isMatrixValue($k)) - ) { - } else { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { - if (is_bool($arg)) { - $arg = (int) $arg; - } elseif (is_string($arg)) { - $arg = 0; - } - $summerA += ($arg * $arg); - $summerB += $arg; - ++$aCount; - } - } - } - - if ($aCount > 1) { - $summerA *= $aCount; - $summerB *= $summerB; - $returnValue = ($summerA - $summerB) / ($aCount * ($aCount - 1)); - } - - return $returnValue; + return Variances::VARA(...$args); } /** @@ -3590,39 +3245,18 @@ class Statistical * Excel Function: * VARP(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Variances::VARP() + * Use the VARP() method in the Statistical\Variances class instead + * * @param mixed ...$args Data values * * @return float|string (string if result is an error) */ public static function VARP(...$args) { - // Return value - $returnValue = Functions::DIV0(); - - $summerA = $summerB = 0; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - $aCount = 0; - foreach ($aArgs as $arg) { - if (is_bool($arg)) { - $arg = (int) $arg; - } - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $summerA += ($arg * $arg); - $summerB += $arg; - ++$aCount; - } - } - - if ($aCount > 0) { - $summerA *= $aCount; - $summerB *= $summerB; - $returnValue = ($summerA - $summerB) / ($aCount * $aCount); - } - - return $returnValue; + return Variances::VARP(...$args); } /** @@ -3633,51 +3267,18 @@ class Statistical * Excel Function: * VARPA(value1[,value2[, ...]]) * + * @Deprecated 1.17.0 + * + * @see Statistical\Variances::VARPA() + * Use the VARPA() method in the Statistical\Variances class instead + * * @param mixed ...$args Data values * * @return float|string (string if result is an error) */ public static function VARPA(...$args) { - $returnValue = Functions::DIV0(); - - $summerA = $summerB = 0; - - // Loop through arguments - $aArgs = Functions::flattenArrayIndexed($args); - $aCount = 0; - foreach ($aArgs as $k => $arg) { - if ( - (is_string($arg)) && - (Functions::isValue($k)) - ) { - return Functions::VALUE(); - } elseif ( - (is_string($arg)) && - (!Functions::isMatrixValue($k)) - ) { - } else { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { - if (is_bool($arg)) { - $arg = (int) $arg; - } elseif (is_string($arg)) { - $arg = 0; - } - $summerA += ($arg * $arg); - $summerB += $arg; - ++$aCount; - } - } - } - - if ($aCount > 0) { - $summerA *= $aCount; - $summerB *= $summerB; - $returnValue = ($summerA - $summerB) / ($aCount * $aCount); - } - - return $returnValue; + return Variances::VARPA(...$args); } /** @@ -3734,10 +3335,10 @@ class Statistical $sigma = Functions::flattenSingleValue($sigma); if ($sigma === null) { - $sigma = self::STDEV($dataSet); + $sigma = StandardDeviations::STDEV($dataSet); } $n = count($dataSet); - return 1 - self::NORMSDIST((self::AVERAGE($dataSet) - $m0) / ($sigma / sqrt($n))); + return 1 - self::NORMSDIST((Averages::AVERAGE($dataSet) - $m0) / ($sigma / sqrt($n))); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/AggregateBase.php b/src/PhpSpreadsheet/Calculation/Statistical/AggregateBase.php new file mode 100644 index 00000000..75c012dc --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/AggregateBase.php @@ -0,0 +1,50 @@ + $arg) { + $arg = self::testAcceptedBoolean($arg, $k); + // Is it a numeric value? + // 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(); + } + if (self::isAcceptedCountable($arg, $k)) { + $returnValue += abs($arg - $aMean); + ++$aCount; + } + } + + // Return + if ($aCount === 0) { + return Functions::DIV0(); + } + + return $returnValue / $aCount; + } + + /** + * AVERAGE. + * + * Returns the average (arithmetic mean) of the arguments + * + * Excel Function: + * AVERAGE(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function AVERAGE(...$args) + { + $returnValue = $aCount = 0; + + // Loop through arguments + foreach (Functions::flattenArrayIndexed($args) as $k => $arg) { + $arg = self::testAcceptedBoolean($arg, $k); + // Is it a numeric value? + // 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(); + } + if (self::isAcceptedCountable($arg, $k)) { + $returnValue += $arg; + ++$aCount; + } + } + + // Return + if ($aCount > 0) { + return $returnValue / $aCount; + } + + return Functions::DIV0(); + } + + /** + * AVERAGEA. + * + * Returns the average of its arguments, including numbers, text, and logical values + * + * Excel Function: + * AVERAGEA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function AVERAGEA(...$args) + { + $returnValue = null; + + $aCount = 0; + // Loop through arguments + foreach (Functions::flattenArrayIndexed($args) as $k => $arg) { + if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { + } else { + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + if (is_bool($arg)) { + $arg = (int) $arg; + } elseif (is_string($arg)) { + $arg = 0; + } + $returnValue += $arg; + ++$aCount; + } + } + } + + if ($aCount > 0) { + return $returnValue / $aCount; + } + + return Functions::DIV0(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Counts.php b/src/PhpSpreadsheet/Calculation/Statistical/Counts.php new file mode 100644 index 00000000..13e7af79 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Counts.php @@ -0,0 +1,95 @@ + $arg) { + $arg = self::testAcceptedBoolean($arg, $k); + // Is it a numeric value? + // 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 (self::isAcceptedCountable($arg, $k)) { + ++$returnValue; + } + } + + return $returnValue; + } + + /** + * COUNTA. + * + * Counts the number of cells that are not empty within the list of arguments + * + * Excel Function: + * COUNTA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return int + */ + public static function COUNTA(...$args) + { + $returnValue = 0; + + // Loop through arguments + $aArgs = Functions::flattenArrayIndexed($args); + foreach ($aArgs as $k => $arg) { + // Nulls are counted if literals, but not if cell values + if ($arg !== null || (!Functions::isCellValue($k))) { + ++$returnValue; + } + } + + return $returnValue; + } + + /** + * COUNTBLANK. + * + * Counts the number of empty cells within the list of arguments + * + * Excel Function: + * COUNTBLANK(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return int + */ + public static function COUNTBLANK(...$args) + { + $returnValue = 0; + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + foreach ($aArgs as $arg) { + // Is it a blank cell? + if (($arg === null) || ((is_string($arg)) && ($arg == ''))) { + ++$returnValue; + } + } + + return $returnValue; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php b/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php new file mode 100644 index 00000000..bd17b062 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/MaxMinBase.php @@ -0,0 +1,17 @@ + $returnValue)) { + $returnValue = $arg; + } + } + } + + if ($returnValue === null) { + return 0; + } + + return $returnValue; + } + + /** + * MAXA. + * + * Returns the greatest value in a list of arguments, including numbers, text, and logical values + * + * Excel Function: + * MAXA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float + */ + public static function MAXA(...$args) + { + $returnValue = null; + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + foreach ($aArgs as $arg) { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + if (($returnValue === null) || ($arg > $returnValue)) { + $returnValue = $arg; + } + } + } + + if ($returnValue === null) { + return 0; + } + + return $returnValue; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php b/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php new file mode 100644 index 00000000..bd46882e --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Minimum.php @@ -0,0 +1,78 @@ + $arg) { + if ( + (is_bool($arg)) && + ((!Functions::isCellValue($k)) || (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) + ) { + $arg = (int) $arg; + } + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $returnValue += ($arg - $aMean) ** 2; + ++$aCount; + } + } + + if ($aCount > 0) { + return sqrt($returnValue / $aCount); + } + } + + return Functions::DIV0(); + } + + /** + * STDEVA. + * + * Estimates standard deviation based on a sample, including numbers, text, and logical values + * + * Excel Function: + * STDEVA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string + */ + public static function STDEVA(...$args) + { + $aArgs = Functions::flattenArrayIndexed($args); + + $aMean = Averages::AVERAGEA($aArgs); + + if (!is_string($aMean)) { + $returnValue = 0.0; + $aCount = -1; + + foreach ($aArgs as $k => $arg) { + if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { + } else { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + $returnValue += ($arg - $aMean) ** 2; + ++$aCount; + } + } + } + + if ($aCount > 0) { + return sqrt($returnValue / $aCount); + } + } + + return Functions::DIV0(); + } + + /** + * STDEVP. + * + * Calculates standard deviation based on the entire population + * + * Excel Function: + * STDEVP(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string + */ + public static function STDEVP(...$args) + { + $aArgs = Functions::flattenArrayIndexed($args); + + $aMean = Averages::AVERAGE($aArgs); + + if (!is_string($aMean)) { + $returnValue = 0.0; + $aCount = 0; + + foreach ($aArgs as $k => $arg) { + if ( + (is_bool($arg)) && + ((!Functions::isCellValue($k)) || (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) + ) { + $arg = (int) $arg; + } + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $returnValue += ($arg - $aMean) ** 2; + ++$aCount; + } + } + + if ($aCount > 0) { + return sqrt($returnValue / $aCount); + } + } + + return Functions::DIV0(); + } + + /** + * STDEVPA. + * + * Calculates standard deviation based on the entire population, including numbers, text, and logical values + * + * Excel Function: + * STDEVPA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string + */ + public static function STDEVPA(...$args) + { + $aArgs = Functions::flattenArrayIndexed($args); + + $aMean = Averages::AVERAGEA($aArgs); + + if (!is_string($aMean)) { + $returnValue = 0.0; + $aCount = 0; + + foreach ($aArgs as $k => $arg) { + if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { + } else { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + $returnValue += ($arg - $aMean) ** 2; + ++$aCount; + } + } + } + + if ($aCount > 0) { + return sqrt($returnValue / $aCount); + } + } + + return Functions::DIV0(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php b/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php new file mode 100644 index 00000000..9762ec84 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php @@ -0,0 +1,26 @@ + 1) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * ($aCount - 1)); + } + + return $returnValue; + } + + /** + * VARA. + * + * Estimates variance based on a sample, including numbers, text, and logical values + * + * Excel Function: + * VARA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function VARA(...$args) + { + $returnValue = Functions::DIV0(); + + $summerA = $summerB = 0.0; + + // Loop through arguments + $aArgs = Functions::flattenArrayIndexed($args); + $aCount = 0; + foreach ($aArgs as $k => $arg) { + if ((is_string($arg)) && (Functions::isValue($k))) { + return Functions::VALUE(); + } elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { + } else { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + $summerA += ($arg * $arg); + $summerB += $arg; + ++$aCount; + } + } + } + + if ($aCount > 1) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * ($aCount - 1)); + } + + return $returnValue; + } + + /** + * VARP. + * + * Calculates variance based on the entire population + * + * Excel Function: + * VARP(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function VARP(...$args) + { + // Return value + $returnValue = Functions::DIV0(); + + $summerA = $summerB = 0.0; + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + $aCount = 0; + foreach ($aArgs as $arg) { + $arg = self::datatypeAdjustmentBooleans($arg); + // Is it a numeric value? + if ((is_numeric($arg)) && (!is_string($arg))) { + $summerA += ($arg * $arg); + $summerB += $arg; + ++$aCount; + } + } + + if ($aCount > 0) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * $aCount); + } + + return $returnValue; + } + + /** + * VARPA. + * + * Calculates variance based on the entire population, including numbers, text, and logical values + * + * Excel Function: + * VARPA(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string (string if result is an error) + */ + public static function VARPA(...$args) + { + $returnValue = Functions::DIV0(); + + $summerA = $summerB = 0.0; + + // Loop through arguments + $aArgs = Functions::flattenArrayIndexed($args); + $aCount = 0; + foreach ($aArgs as $k => $arg) { + if ((is_string($arg)) && (Functions::isValue($k))) { + return Functions::VALUE(); + } elseif ((is_string($arg)) && (!Functions::isMatrixValue($k))) { + } else { + // Is it a numeric value? + if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) & ($arg != '')))) { + $arg = self::datatypeAdjustmentAllowStrings($arg); + $summerA += ($arg * $arg); + $summerB += $arg; + ++$aCount; + } + } + } + + if ($aCount > 0) { + $summerA *= $aCount; + $summerB *= $summerB; + + return ($summerA - $summerB) / ($aCount * $aCount); + } + + return $returnValue; + } +} diff --git a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php index 7530b1ef..56e917e3 100644 --- a/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php +++ b/src/PhpSpreadsheet/Writer/Pdf/Tcpdf.php @@ -24,7 +24,7 @@ class Tcpdf extends Pdf * * @param string $orientation Page orientation * @param string $unit Unit measure - * @param string $paperSize Paper size + * @param array|string $paperSize Paper size * * @return \TCPDF implementation */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php new file mode 100644 index 00000000..9115db46 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php @@ -0,0 +1,26 @@ + Date: Tue, 2 Mar 2021 18:01:39 +0100 Subject: [PATCH 14/89] Statistics more unit tests (#1889) * Additional unit tests --- .../Calculation/Statistical.php | 14 +++++--- .../Functions/LookupRef/ColumnTest.php | 31 +++++++++++++++++ .../Functions/LookupRef/RowTest.php | 31 +++++++++++++++++ .../Functions/Statistical/SkewTest.php | 31 +++++++++++++++++ .../Functions/Statistical/TrimMeanTest.php | 32 +++++++++++++++++ tests/data/Calculation/LookupRef/COLUMN.php | 12 +++++++ tests/data/Calculation/LookupRef/ROW.php | 12 +++++++ tests/data/Calculation/Statistical/COVAR.php | 10 ++++++ .../data/Calculation/Statistical/FORECAST.php | 18 ++++++++++ .../Calculation/Statistical/INTERCEPT.php | 10 ++++++ tests/data/Calculation/Statistical/RSQ.php | 10 ++++++ tests/data/Calculation/Statistical/SKEW.php | 20 +++++++++++ tests/data/Calculation/Statistical/SLOPE.php | 10 ++++++ .../data/Calculation/Statistical/TRIMMEAN.php | 34 +++++++++++++++++++ 14 files changed, 271 insertions(+), 4 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/SkewTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrimMeanTest.php create mode 100644 tests/data/Calculation/LookupRef/COLUMN.php create mode 100644 tests/data/Calculation/LookupRef/ROW.php create mode 100644 tests/data/Calculation/Statistical/SKEW.php create mode 100644 tests/data/Calculation/Statistical/TRIMMEAN.php diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index dc9c5b44..0e15ecf4 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -2753,13 +2753,16 @@ class Statistical $mean = Averages::AVERAGE($aArgs); $stdDev = StandardDeviations::STDEV($aArgs); + if ($stdDev === 0.0 || is_string($stdDev)) { + return Functions::DIV0(); + } + $count = $summer = 0; // Loop through arguments foreach ($aArgs as $k => $arg) { - if ( - (is_bool($arg)) && - (!Functions::isMatrixValue($k)) - ) { + if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { + } elseif (!is_numeric($arg)) { + return Functions::VALUE(); } else { // Is it a numeric value? if ((is_numeric($arg)) && (!is_string($arg))) { @@ -3173,6 +3176,7 @@ class Statistical if (($percent < 0) || ($percent > 1)) { return Functions::NAN(); } + $mArgs = []; foreach ($aArgs as $arg) { // Is it a numeric value? @@ -3180,8 +3184,10 @@ class Statistical $mArgs[] = $arg; } } + $discard = floor(Counts::COUNT($mArgs) * $percent / 2); sort($mArgs); + for ($i = 0; $i < $discard; ++$i) { array_pop($mArgs); array_shift($mArgs); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php new file mode 100644 index 00000000..203ee479 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php @@ -0,0 +1,31 @@ + Date: Tue, 2 Mar 2021 18:51:13 +0100 Subject: [PATCH 15/89] Changelog --- CHANGELOG.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ab843c2..8541d100 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,28 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Nothing. + +### Changed + +- Nothing. + +### Deprecated + +- Nothing. + +### Removed + +- Nothing. + +### Fixed + +- Nothing. + +## 1.17.0 - 2021-03-01 + +### Added + - Implementation of the Excel `AVERAGEIFS()` functions as part of a restructuring of Database functions and Conditional Statistical functions. - Support for date values and percentages in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1875](https://github.com/PHPOffice/PhpSpreadsheet/pull/1875) - Support for booleans, and for wildcard text search in query parameters for Database functions, and the IF expressions in functions like COUNTIF() and AVERAGEIF(). [#1876](https://github.com/PHPOffice/PhpSpreadsheet/pull/1876) @@ -46,6 +68,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Nothing. ### Fixed + - Avoid Duplicate Titles When Reading Multiple HTML Files.[Issue #1823](https://github.com/PHPOffice/PhpSpreadsheet/issues/1823) [PR #1829](https://github.com/PHPOffice/PhpSpreadsheet/pull/1829) - Fixed issue with Worksheet's `getCell()` method when trying to get a cell by defined name. [#1858](https://github.com/PHPOffice/PhpSpreadsheet/issues/1858) - Fix possible endless loop in NumberFormat Masks [#1792](https://github.com/PHPOffice/PhpSpreadsheet/issues/1792) From c55269cb06911575a126dc225a05c0e4626e5fb4 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Tue, 2 Mar 2021 18:54:11 +0100 Subject: [PATCH 16/89] Changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8541d100..419869bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Nothing. -## 1.17.0 - 2021-03-01 +## 1.17.1 - 2021-03-01 ### Added From 04e7c3075803f6d6e6bbf1ce560badae356e6225 Mon Sep 17 00:00:00 2001 From: oleibman Date: Wed, 3 Mar 2021 01:52:11 -0800 Subject: [PATCH 17/89] Fix Two 32-bit Timestamp Problems, and Minor getFormattedValue Bug (#1891) I ran the test suite using 32-bit PHP. There were 2 places where changes were needed due to 32-bit timestamps. Reader\\Xml.php was using strtotime as an intermediate step in converting a string timestamp to an Excel timestamp. The XML file type stores pure timestamps (i.e. no date portion) as, e.g., 1899-12-31T02:30:00.000, and that value causes an error using strtotime on a 32-bit system. However, it is sufficient to use that value in a DateTime constructor, and that will work for 32- and 64-bit. There was no test for that particular cell, so I added one to the XML read test. And that's when I discovered the getFormattedValue bug. The cell's format is `hh":"mm":"ss`. The quotes around the colons are disrupting the formatting. PhpSpreadsheet formats the cell by converting the Excel format to a Php Date format, in this case `H\:m\:s`. That's a problem, since Excel thinks 'm' means *minutes*, but PHP thinks it means *months*. This is not a problem when the colon is not quoted; there are ample tests for that. I added my best guess as to how to recognize this situation, changing `\:m` to `:i`. The XML read test now succeeds, and no other tests were broken by this change. Test Shared\\DateTest had one test where the expected result of converting to a Unix timestamp exceeds 2**32. Since a Unix timestamp is strictly an int, that test fails on a 32-bit system. In the discussion regarding recently merged PR #1870, it was felt that the user base might still be using the functions that convert to and from a timestamp. So, we should not drop this test, but, since it cannot succeed on a 32-bit system, I changed it to be skipped whenever the expected result exceeded PHP_INT_MAX. There are 3 "toTimestamp" functions within that test. Only one of these had been affected, but I thought it was a good idea to add additional tests to the others to demonstrate this condition. In the course of testing, I also discovered some 32-bit problems with bitwise and base-conversion functions. I am preparing separate PRs to deal with those. --- src/PhpSpreadsheet/Reader/Xml.php | 5 ++++- src/PhpSpreadsheet/Style/NumberFormat.php | 4 ++++ .../PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php | 4 ++++ tests/PhpSpreadsheetTests/Shared/DateTest.php | 13 +++++++++++++ tests/data/Shared/Date/ExcelToTimestamp1900.php | 7 +++++++ tests/data/Shared/Date/ExcelToTimestamp1904.php | 5 +++++ 6 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index f38a9515..b1df0ef5 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Reader; +use DateTime; +use DateTimeZone; use PhpOffice\PhpSpreadsheet\Cell\AddressHelper; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Cell\DataType; @@ -557,7 +559,8 @@ class Xml extends BaseReader break; case 'DateTime': $type = DataType::TYPE_NUMERIC; - $cellValue = Date::PHPToExcel(strtotime($cellValue . ' UTC')); + $dateTime = new DateTime($cellValue, new DateTimeZone('UTC')); + $cellValue = Date::PHPToExcel($dateTime); break; case 'Error': diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index a96d2ac3..a623657b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -502,6 +502,10 @@ class NumberFormat extends Supervisor $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $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); $value = $dateObj->format($format); } diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php index 969571f2..3783c1a9 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlLoadTest.php @@ -36,6 +36,10 @@ class XmlLoadTest extends TestCase self::assertEquals('# ?0/??0', $sheet->getCell('A11')->getStyle()->getNumberFormat()->getFormatCode()); // Same pattern, same value, different display in Gnumeric vs Excel //self::assertEquals('1 1/2', $sheet->getCell('A11')->getFormattedValue()); + self::assertEquals('hh":"mm":"ss', $sheet->getCell('A13')->getStyle()->getNumberFormat()->getFormatCode()); + self::assertEquals('02:30:00', $sheet->getCell('A13')->getFormattedValue()); + self::assertEquals('d/m/yy hh":"mm', $sheet->getCell('A15')->getStyle()->getNumberFormat()->getFormatCode()); + self::assertEquals('19/12/60 01:30', $sheet->getCell('A15')->getFormattedValue()); self::assertEquals('=B1+C1', $sheet->getCell('H1')->getValue()); self::assertEquals('=E2&F2', $sheet->getCell('J2')->getValue()); diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php index 7254635e..550e2f6a 100644 --- a/tests/PhpSpreadsheetTests/Shared/DateTest.php +++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php @@ -8,16 +8,20 @@ use PHPUnit\Framework\TestCase; class DateTest extends TestCase { + private $excelCalendar; + private $dttimezone; protected function setUp(): void { $this->dttimezone = Date::getDefaultTimeZone(); + $this->excelCalendar = Date::getExcelCalendar(); } protected function tearDown(): void { Date::setDefaultTimeZone($this->dttimezone); + Date::setExcelCalendar($this->excelCalendar); } public function testSetExcelCalendar(): void @@ -47,6 +51,9 @@ class DateTest extends TestCase */ public function testDateTimeExcelToTimestamp1900($expectedResult, ...$args): void { + if (is_numeric($expectedResult) && ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN)) { + self::markTestSkipped('Test invalid on 32-bit system.'); + } Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); $result = Date::excelToTimestamp(...$args); @@ -119,6 +126,9 @@ class DateTest extends TestCase */ public function testDateTimeExcelToTimestamp1904($expectedResult, ...$args): void { + if (is_numeric($expectedResult) && ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN)) { + self::markTestSkipped('Test invalid on 32-bit system.'); + } Date::setExcelCalendar(Date::CALENDAR_MAC_1904); $result = Date::excelToTimestamp(...$args); @@ -171,6 +181,9 @@ class DateTest extends TestCase */ public function testDateTimeExcelToTimestamp1900Timezone($expectedResult, ...$args): void { + if (is_numeric($expectedResult) && ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN)) { + self::markTestSkipped('Test invalid on 32-bit system.'); + } Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); $result = Date::excelToTimestamp(...$args); diff --git a/tests/data/Shared/Date/ExcelToTimestamp1900.php b/tests/data/Shared/Date/ExcelToTimestamp1900.php index ab79731e..ffcd194f 100644 --- a/tests/data/Shared/Date/ExcelToTimestamp1900.php +++ b/tests/data/Shared/Date/ExcelToTimestamp1900.php @@ -72,4 +72,11 @@ return [ 10666, 0.12345, ], + // 29-Apr-2038 00:00:00 beyond PHP 32-bit Latest Date + [ + 2156112000, + 50524, + ], + [-2147483648, -2147483648 / 86400], // Okay on 64- and 32-bit systems + [-2147483649, -2147483649 / 86400], // Skipped test on 32-bit ]; diff --git a/tests/data/Shared/Date/ExcelToTimestamp1904.php b/tests/data/Shared/Date/ExcelToTimestamp1904.php index 1c013ab4..fc55238a 100644 --- a/tests/data/Shared/Date/ExcelToTimestamp1904.php +++ b/tests/data/Shared/Date/ExcelToTimestamp1904.php @@ -42,4 +42,9 @@ return [ gmmktime(13, 02, 13, 1, 1, 1904), // 32-bit safe - no Y2038 problem 0.54321, ], + // 29-Apr-2038 00:00:00 beyond PHP 32-bit Latest Date + [ + 2156112000, + 49062, + ], ]; From 70e371189c9030b96bad9723ba38d46268365292 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 3 Mar 2021 12:51:50 +0100 Subject: [PATCH 18/89] Move the trend functions from Statistical and into their own group class (#1890) * Move the trend functions from Statistical and into their own group class * Additional LINEST()/LOGEST() tests, and fix for the returned array --- .../Calculation/Calculation.php | 22 +- .../Calculation/Statistical.php | 283 +++----------- .../Calculation/Statistical/Trends.php | 359 ++++++++++++++++++ tests/data/Calculation/Statistical/LINEST.php | 38 ++ tests/data/Calculation/Statistical/LOGEST.php | 7 + 5 files changed, 470 insertions(+), 239 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Trends.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index bc0baa4d..7fd07355 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -599,7 +599,7 @@ class Calculation ], 'CORREL' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CORREL'], + 'functionCall' => [Statistical\Trends::class, 'CORREL'], 'argumentCount' => '2', ], 'COS' => [ @@ -679,12 +679,12 @@ class Calculation ], 'COVAR' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'COVAR'], + 'functionCall' => [Statistical\Trends::class, 'COVAR'], 'argumentCount' => '2', ], 'COVARIANCE.P' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'COVAR'], + 'functionCall' => [Statistical\Trends::class, 'COVAR'], 'argumentCount' => '2', ], 'COVARIANCE.S' => [ @@ -1084,7 +1084,7 @@ class Calculation ], 'FORECAST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'FORECAST'], + 'functionCall' => [Statistical\Trends::class, 'FORECAST'], 'argumentCount' => '3', ], 'FORECAST.ETS' => [ @@ -1109,7 +1109,7 @@ class Calculation ], 'FORECAST.LINEAR' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'FORECAST'], + 'functionCall' => [Statistical\Trends::class, 'FORECAST'], 'argumentCount' => '3', ], 'FORMULATEXT' => [ @@ -1423,7 +1423,7 @@ class Calculation ], 'INTERCEPT' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'INTERCEPT'], + 'functionCall' => [Statistical\Trends::class, 'INTERCEPT'], 'argumentCount' => '2', ], 'INTRATE' => [ @@ -1560,7 +1560,7 @@ class Calculation ], 'LINEST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'LINEST'], + 'functionCall' => [Statistical\Trends::class, 'LINEST'], 'argumentCount' => '1-4', ], 'LN' => [ @@ -1580,7 +1580,7 @@ class Calculation ], 'LOGEST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'LOGEST'], + 'functionCall' => [Statistical\Trends::class, 'LOGEST'], 'argumentCount' => '1-4', ], 'LOGINV' => [ @@ -2143,7 +2143,7 @@ class Calculation ], 'RSQ' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'RSQ'], + 'functionCall' => [Statistical\Trends::class, 'RSQ'], 'argumentCount' => '2', ], 'RTD' => [ @@ -2228,7 +2228,7 @@ class Calculation ], 'SLOPE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'SLOPE'], + 'functionCall' => [Statistical\Trends::class, 'SLOPE'], 'argumentCount' => '2', ], 'SMALL' => [ @@ -2293,7 +2293,7 @@ class Calculation ], 'STEYX' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'STEYX'], + 'functionCall' => [Statistical\Trends::class, 'STEYX'], 'argumentCount' => '2', ], 'SUBSTITUTE' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 0e15ecf4..b2b9ad50 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -9,6 +9,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Maximum; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Minimum; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Permutations; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Trends; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances; use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend; @@ -21,33 +22,6 @@ class Statistical const MAX_ITERATIONS = 256; const SQRT2PI = 2.5066282746310005024157652848110452530069867406099; - private static function checkTrendArrays(&$array1, &$array2) - { - if (!is_array($array1)) { - $array1 = [$array1]; - } - if (!is_array($array2)) { - $array2 = [$array2]; - } - - $array1 = Functions::flattenArray($array1); - $array2 = Functions::flattenArray($array2); - foreach ($array1 as $key => $value) { - if ((is_bool($value)) || (is_string($value)) || ($value === null)) { - unset($array1[$key], $array2[$key]); - } - } - foreach ($array2 as $key => $value) { - if ((is_bool($value)) || (is_string($value)) || ($value === null)) { - unset($array1[$key], $array2[$key]); - } - } - $array1 = array_merge($array1); - $array2 = array_merge($array2); - - return true; - } - /** * Incomplete beta function. * @@ -890,6 +864,11 @@ class Statistical * * Returns covariance, the average of the products of deviations for each data point pair. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::CORREL() + * Use the CORREL() method in the Statistical\Trends class instead + * * @param mixed $yValues array of mixed Data Series Y * @param null|mixed $xValues array of mixed Data Series X * @@ -897,24 +876,7 @@ class Statistical */ public static function CORREL($yValues, $xValues = null) { - if (($xValues === null) || (!is_array($yValues)) || (!is_array($xValues))) { - return Functions::VALUE(); - } - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return Functions::DIV0(); - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); - - return $bestFitLinear->getCorrelation(); + return Trends::CORREL($xValues, $yValues); } /** @@ -1033,6 +995,11 @@ class Statistical * * Returns covariance, the average of the products of deviations for each data point pair. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::COVAR() + * Use the COVAR() method in the Statistical\Trends class instead + * * @param mixed $yValues array of mixed Data Series Y * @param mixed $xValues array of mixed Data Series X * @@ -1040,21 +1007,7 @@ class Statistical */ public static function COVAR($yValues, $xValues) { - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return Functions::DIV0(); - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); - - return $bestFitLinear->getCovariance(); + return Trends::COVAR($yValues, $xValues); } /** @@ -1380,6 +1333,11 @@ class Statistical * * Calculates, or predicts, a future value by using existing values. The predicted value is a y-value for a given x-value. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::FORECAST() + * Use the FORECAST() method in the Statistical\Trends class instead + * * @param float $xValue Value of X for which we want to find Y * @param mixed $yValues array of mixed Data Series Y * @param mixed $xValues of mixed Data Series X @@ -1388,24 +1346,7 @@ class Statistical */ public static function FORECAST($xValue, $yValues, $xValues) { - $xValue = Functions::flattenSingleValue($xValue); - if (!is_numeric($xValue)) { - return Functions::VALUE(); - } elseif (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return Functions::DIV0(); - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); - - return $bestFitLinear->getValueOfYForX($xValue); + return Trends::FORECAST($xValue, $yValues, $xValues); } /** @@ -1722,6 +1663,11 @@ class Statistical * * Calculates the point at which a line will intersect the y-axis by using existing x-values and y-values. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::INTERCEPT() + * Use the INTERCEPT() method in the Statistical\Trends class instead + * * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @@ -1729,21 +1675,7 @@ class Statistical */ public static function INTERCEPT($yValues, $xValues) { - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return Functions::DIV0(); - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); - - return $bestFitLinear->getIntersect(); + return Trends::INTERCEPT($yValues, $xValues); } /** @@ -1838,6 +1770,11 @@ class Statistical * Calculates the statistics for a line by using the "least squares" method to calculate a straight line that best fits your data, * and then returns an array that describes the line. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::LINEST() + * Use the LINEST() method in the Statistical\Trends class instead + * * @param mixed[] $yValues Data Series Y * @param null|mixed[] $xValues Data Series X * @param bool $const a logical value specifying whether to force the intersect to equal 0 @@ -1847,48 +1784,7 @@ class Statistical */ public static function LINEST($yValues, $xValues = null, $const = true, $stats = false) { - $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); - $stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); - if ($xValues === null) { - $xValues = range(1, count(Functions::flattenArray($yValues))); - } - - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return 0; - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); - if ($stats) { - return [ - [ - $bestFitLinear->getSlope(), - $bestFitLinear->getSlopeSE(), - $bestFitLinear->getGoodnessOfFit(), - $bestFitLinear->getF(), - $bestFitLinear->getSSRegression(), - ], - [ - $bestFitLinear->getIntersect(), - $bestFitLinear->getIntersectSE(), - $bestFitLinear->getStdevOfResiduals(), - $bestFitLinear->getDFResiduals(), - $bestFitLinear->getSSResiduals(), - ], - ]; - } - - return [ - $bestFitLinear->getSlope(), - $bestFitLinear->getIntersect(), - ]; + return Trends::LINEST($yValues, $xValues, $const, $stats); } /** @@ -1897,6 +1793,11 @@ class Statistical * Calculates an exponential curve that best fits the X and Y data series, * and then returns an array that describes the line. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::LOGEST() + * Use the LOGEST() method in the Statistical\Trends class instead + * * @param mixed[] $yValues Data Series Y * @param null|mixed[] $xValues Data Series X * @param bool $const a logical value specifying whether to force the intersect to equal 0 @@ -1906,54 +1807,7 @@ class Statistical */ public static function LOGEST($yValues, $xValues = null, $const = true, $stats = false) { - $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); - $stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); - if ($xValues === null) { - $xValues = range(1, count(Functions::flattenArray($yValues))); - } - - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - foreach ($yValues as $value) { - if ($value <= 0.0) { - return Functions::NAN(); - } - } - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return 1; - } - - $bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); - if ($stats) { - return [ - [ - $bestFitExponential->getSlope(), - $bestFitExponential->getSlopeSE(), - $bestFitExponential->getGoodnessOfFit(), - $bestFitExponential->getF(), - $bestFitExponential->getSSRegression(), - ], - [ - $bestFitExponential->getIntersect(), - $bestFitExponential->getIntersectSE(), - $bestFitExponential->getStdevOfResiduals(), - $bestFitExponential->getDFResiduals(), - $bestFitExponential->getSSResiduals(), - ], - ]; - } - - return [ - $bestFitExponential->getSlope(), - $bestFitExponential->getIntersect(), - ]; + return Trends::LOGEST($yValues, $xValues, $const, $stats); } /** @@ -2711,6 +2565,11 @@ class Statistical * * Returns the square of the Pearson product moment correlation coefficient through data points in known_y's and known_x's. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::RSQ() + * Use the RSQ() method in the Statistical\Trends class instead + * * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @@ -2718,21 +2577,7 @@ class Statistical */ public static function RSQ($yValues, $xValues) { - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return Functions::DIV0(); - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); - - return $bestFitLinear->getGoodnessOfFit(); + return Trends::RSQ($yValues, $xValues); } /** @@ -2784,6 +2629,11 @@ class Statistical * * Returns the slope of the linear regression line through data points in known_y's and known_x's. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::SLOPE() + * Use the SLOPE() method in the Statistical\Trends class instead + * * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @@ -2791,21 +2641,7 @@ class Statistical */ public static function SLOPE($yValues, $xValues) { - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return Functions::DIV0(); - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); - - return $bestFitLinear->getSlope(); + return Trends::SLOPE($yValues, $xValues); } /** @@ -2971,6 +2807,11 @@ class Statistical /** * STEYX. * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::STEYX() + * Use the STEYX() method in the Statistical\Trends class instead + * * Returns the standard error of the predicted y-value for each x in the regression. * * @param mixed[] $yValues Data Series Y @@ -2980,21 +2821,7 @@ class Statistical */ public static function STEYX($yValues, $xValues) { - if (!self::checkTrendArrays($yValues, $xValues)) { - return Functions::VALUE(); - } - $yValueCount = count($yValues); - $xValueCount = count($xValues); - - if (($yValueCount == 0) || ($yValueCount != $xValueCount)) { - return Functions::NA(); - } elseif ($yValueCount == 1) { - return Functions::DIV0(); - } - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); - - return $bestFitLinear->getStdevOfResiduals(); + return Trends::STEYX($yValues, $xValues); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php new file mode 100644 index 00000000..032b4625 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -0,0 +1,359 @@ + $value) { + if ((is_bool($value)) || (is_string($value)) || ($value === null)) { + unset($array1[$key], $array2[$key]); + } + } + } + + private static function checkTrendArrays(&$array1, &$array2): void + { + if (!is_array($array1)) { + $array1 = [$array1]; + } + if (!is_array($array2)) { + $array2 = [$array2]; + } + + $array1 = Functions::flattenArray($array1); + $array2 = Functions::flattenArray($array2); + + self::filterTrendValues($array1, $array2); + self::filterTrendValues($array2, $array1); + + // Reset the array indexes + $array1 = array_merge($array1); + $array2 = array_merge($array2); + } + + protected static function validateTrendArrays(array $yValues, array $xValues): void + { + $yValueCount = count($yValues); + $xValueCount = count($xValues); + + if (($yValueCount === 0) || ($yValueCount !== $xValueCount)) { + throw new Exception(Functions::NA()); + } elseif ($yValueCount === 1) { + throw new Exception(Functions::DIV0()); + } + } + + /** + * CORREL. + * + * Returns covariance, the average of the products of deviations for each data point pair. + * + * @param mixed $yValues array of mixed Data Series Y + * @param null|mixed $xValues array of mixed Data Series X + * + * @return float|string + */ + public static function CORREL($yValues, $xValues = null) + { + if (($xValues === null) || (!is_array($yValues)) || (!is_array($xValues))) { + return Functions::VALUE(); + } + + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getCorrelation(); + } + + /** + * COVAR. + * + * Returns covariance, the average of the products of deviations for each data point pair. + * + * @param mixed $yValues array of mixed Data Series Y + * @param mixed $xValues array of mixed Data Series X + * + * @return float|string + */ + public static function COVAR($yValues, $xValues) + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getCovariance(); + } + + /** + * FORECAST. + * + * Calculates, or predicts, a future value by using existing values. + * The predicted value is a y-value for a given x-value. + * + * @param float $xValue Value of X for which we want to find Y + * @param mixed $yValues array of mixed Data Series Y + * @param mixed $xValues of mixed Data Series X + * + * @return bool|float|string + */ + public static function FORECAST($xValue, $yValues, $xValues) + { + $xValue = Functions::flattenSingleValue($xValue); + if (!is_numeric($xValue)) { + return Functions::VALUE(); + } + + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getValueOfYForX($xValue); + } + + /** + * INTERCEPT. + * + * Calculates the point at which a line will intersect the y-axis by using existing x-values and y-values. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * + * @return float|string + */ + public static function INTERCEPT($yValues, $xValues) + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getIntersect(); + } + + /** + * LINEST. + * + * Calculates the statistics for a line by using the "least squares" method to calculate a straight line + * that best fits your data, and then returns an array that describes the line. + * + * @param mixed[] $yValues Data Series Y + * @param null|mixed[] $xValues Data Series X + * @param bool $const a logical value specifying whether to force the intersect to equal 0 + * @param bool $stats a logical value specifying whether to return additional regression statistics + * + * @return array|int|string The result, or a string containing an error + */ + public static function LINEST($yValues, $xValues = null, $const = true, $stats = false) + { + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + $stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); + if ($xValues === null) { + $xValues = $yValues; + } + + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); + + if ($stats === true) { + return [ + [ + $bestFitLinear->getSlope(), + $bestFitLinear->getIntersect(), + ], + [ + $bestFitLinear->getSlopeSE(), + $bestFitLinear->getIntersectSE(), + ], + [ + $bestFitLinear->getGoodnessOfFit(), + $bestFitLinear->getStdevOfResiduals(), + ], + [ + $bestFitLinear->getF(), + $bestFitLinear->getDFResiduals(), + ], + [ + $bestFitLinear->getSSRegression(), + $bestFitLinear->getSSResiduals(), + ], + ]; + } + + return [ + $bestFitLinear->getSlope(), + $bestFitLinear->getIntersect(), + ]; + } + + /** + * LOGEST. + * + * Calculates an exponential curve that best fits the X and Y data series, + * and then returns an array that describes the line. + * + * @param mixed[] $yValues Data Series Y + * @param null|mixed[] $xValues Data Series X + * @param bool $const a logical value specifying whether to force the intersect to equal 0 + * @param bool $stats a logical value specifying whether to return additional regression statistics + * + * @return array|int|string The result, or a string containing an error + */ + public static function LOGEST($yValues, $xValues = null, $const = true, $stats = false) + { + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + $stats = ($stats === null) ? false : (bool) Functions::flattenSingleValue($stats); + if ($xValues === null) { + $xValues = $yValues; + } + + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + foreach ($yValues as $value) { + if ($value < 0.0) { + return Functions::NAN(); + } + } + + $bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); + + if ($stats === true) { + return [ + [ + $bestFitExponential->getSlope(), + $bestFitExponential->getIntersect(), + ], + [ + $bestFitExponential->getSlopeSE(), + $bestFitExponential->getIntersectSE(), + ], + [ + $bestFitExponential->getGoodnessOfFit(), + $bestFitExponential->getStdevOfResiduals(), + ], + [ + $bestFitExponential->getF(), + $bestFitExponential->getDFResiduals(), + ], + [ + $bestFitExponential->getSSRegression(), + $bestFitExponential->getSSResiduals(), + ], + ]; + } + + return [ + $bestFitExponential->getSlope(), + $bestFitExponential->getIntersect(), + ]; + } + + /** + * RSQ. + * + * Returns the square of the Pearson product moment correlation coefficient through data points + * in known_y's and known_x's. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * + * @return float|string The result, or a string containing an error + */ + public static function RSQ($yValues, $xValues) + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getGoodnessOfFit(); + } + + /** + * SLOPE. + * + * Returns the slope of the linear regression line through data points in known_y's and known_x's. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * + * @return float|string The result, or a string containing an error + */ + public static function SLOPE($yValues, $xValues) + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getSlope(); + } + + /** + * STEYX. + * + * Returns the standard error of the predicted y-value for each x in the regression. + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * + * @return float|string + */ + public static function STEYX($yValues, $xValues) + { + try { + self::checkTrendArrays($yValues, $xValues); + self::validateTrendArrays($yValues, $xValues); + } catch (Exception $e) { + return $e->getMessage(); + } + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues); + + return $bestFitLinear->getStdevOfResiduals(); + } +} diff --git a/tests/data/Calculation/Statistical/LINEST.php b/tests/data/Calculation/Statistical/LINEST.php index 2b2680c7..9bd28ffe 100644 --- a/tests/data/Calculation/Statistical/LINEST.php +++ b/tests/data/Calculation/Statistical/LINEST.php @@ -8,4 +8,42 @@ return [ true, false, ], + [ + [ + [56.837944664032, 11704.347826086974], + [54.403741747077, 131988.39973671117], + [0.108159322123, 13123.598915116556], + [1.091488562082, 9], + [187985818.1818185, 1550059636.363636], + ], + [142000, 144000, 151000, 150000, 139000, 169000, 126000, 142900, 163000, 169000, 149000], + [2310, 2333, 2356, 2379, 2402, 2425, 2448, 2471, 2494, 2517, 2540], + true, + true, + ], + // [ + // [ + // [-234.2371645, 2553.21066, 12529.76817, 27.64138737, 52317.83051], + // [13.26801148, 530.6691519, 400.0668382, 5.429374042, 12237.3616], + // [0.996747993, 970.5784629, '#N/A', '#N/A', '#N/A'], + // [459.7536742, 6, '#N/A', '#N/A', '#N/A'], + // [1732393319, 5652135.316, '#N/A', '#N/A', '#N/A'], + // ], + // [142000, 144000, 151000, 150000, 139000, 169000, 126000, 142900, 163000, 169000, 149000], + // [ + // [2310, 2, 2, 20], + // [2333, 2, 2, 12], + // [2356, 3, 1.5, 33], + // [2379, 3, 2, 43], + // [2402, 2, 3, 53], + // [2425, 4, 2, 23], + // [2448, 2, 1.5, 99], + // [2471, 2, 2, 34], + // [2494, 3, 3, 23], + // [2517, 4, 4, 55], + // [2540, 2, 3, 22], + // ], + // true, + // true, + // ], ]; diff --git a/tests/data/Calculation/Statistical/LOGEST.php b/tests/data/Calculation/Statistical/LOGEST.php index e74db005..be9e4d72 100644 --- a/tests/data/Calculation/Statistical/LOGEST.php +++ b/tests/data/Calculation/Statistical/LOGEST.php @@ -8,4 +8,11 @@ return [ true, false, ], + [ + [1.482939830687, 2.257475168225], + [2, 3, 6, 8, 12, 15, 25, 34, 50], + [0, 1, 2, 3, 4, 5, 6, 7, 8], + true, + false, + ], ]; From 3e672710cd1c9b96b7029763de5d94954b07f8af Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 3 Mar 2021 18:16:37 +0100 Subject: [PATCH 19/89] Writing defined names without a worksheet reference in Xlsx (#1892) --- src/PhpSpreadsheet/Writer/Xls/Workbook.php | 4 ++-- src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xls/Workbook.php b/src/PhpSpreadsheet/Writer/Xls/Workbook.php index 2b4895f0..831c120b 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xls/Workbook.php @@ -551,8 +551,8 @@ class Workbook extends BIFFwriter $newRange = ''; if (empty($worksheet)) { if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) { - // We need a worksheet - $worksheet = $pDefinedName->getWorksheet()->getTitle(); + // We should have a worksheet + $worksheet = $pDefinedName->getWorksheet() ? $pDefinedName->getWorksheet()->getTitle() : null; } } else { $worksheet = str_replace("''", "'", trim($worksheet, "'")); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index 8c3da827..a1ea02ba 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -99,7 +99,7 @@ class DefinedNames if (empty($worksheet)) { if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) { // We should have a worksheet - $worksheet = $pDefinedName->getWorksheet()->getTitle(); + $worksheet = $pDefinedName->getWorksheet() ? $pDefinedName->getWorksheet()->getTitle() : null; } } else { $worksheet = str_replace("''", "'", trim($worksheet, "'")); From 60023e48f2ade64fe4a52036a09be5c700737d42 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 3 Mar 2021 18:49:46 +0100 Subject: [PATCH 20/89] Refactoring --- .../Writer/Xlsx/DefinedNames.php | 107 ++++++++++-------- 1 file changed, 57 insertions(+), 50 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index a1ea02ba..30aa45a3 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -72,55 +72,7 @@ class DefinedNames $this->objWriter->writeAttribute('localSheetId', $pDefinedName->getScope()->getParent()->getIndex($pDefinedName->getScope())); } - $definedRange = $pDefinedName->getValue(); - $splitCount = preg_match_all( - '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', - $definedRange, - $splitRanges, - PREG_OFFSET_CAPTURE - ); - - $lengths = array_map('strlen', array_column($splitRanges[0], 0)); - $offsets = array_column($splitRanges[0], 1); - - $worksheets = $splitRanges[2]; - $columns = $splitRanges[6]; - $rows = $splitRanges[7]; - - while ($splitCount > 0) { - --$splitCount; - $length = $lengths[$splitCount]; - $offset = $offsets[$splitCount]; - $worksheet = $worksheets[$splitCount][0]; - $column = $columns[$splitCount][0]; - $row = $rows[$splitCount][0]; - - $newRange = ''; - if (empty($worksheet)) { - if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) { - // We should have a worksheet - $worksheet = $pDefinedName->getWorksheet() ? $pDefinedName->getWorksheet()->getTitle() : null; - } - } else { - $worksheet = str_replace("''", "'", trim($worksheet, "'")); - } - if (!empty($worksheet)) { - $newRange = "'" . str_replace("'", "''", $worksheet) . "'!"; - } - - if (!empty($column)) { - $newRange .= $column; - } - if (!empty($row)) { - $newRange .= $row; - } - - $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length); - } - - if (substr($definedRange, 0, 1) === '=') { - $definedRange = substr($definedRange, 1); - } + $definedRange = $this->getDefinedRange($pDefinedName); $this->objWriter->writeRawData($definedRange); @@ -144,7 +96,7 @@ class DefinedNames $range = Coordinate::splitRange($autoFilterRange); $range = $range[0]; // Strip any worksheet ref so we can make the cell ref absolute - [$ws, $range[0]] = Worksheet::extractSheetTitle($range[0], true); + [, $range[0]] = Worksheet::extractSheetTitle($range[0], true); $range[0] = Coordinate::absoluteCoordinate($range[0]); $range[1] = Coordinate::absoluteCoordinate($range[1]); @@ -220,4 +172,59 @@ class DefinedNames $this->objWriter->endElement(); } } + + private function getDefinedRange(DefinedName $pDefinedName): string + { + $definedRange = $pDefinedName->getValue(); + $splitCount = preg_match_all( + '/' . Calculation::CALCULATION_REGEXP_CELLREF_RELATIVE . '/mui', + $definedRange, + $splitRanges, + PREG_OFFSET_CAPTURE + ); + + $lengths = array_map('strlen', array_column($splitRanges[0], 0)); + $offsets = array_column($splitRanges[0], 1); + + $worksheets = $splitRanges[2]; + $columns = $splitRanges[6]; + $rows = $splitRanges[7]; + + while ($splitCount > 0) { + --$splitCount; + $length = $lengths[$splitCount]; + $offset = $offsets[$splitCount]; + $worksheet = $worksheets[$splitCount][0]; + $column = $columns[$splitCount][0]; + $row = $rows[$splitCount][0]; + + $newRange = ''; + if (empty($worksheet)) { + if (($offset === 0) || ($definedRange[$offset - 1] !== ':')) { + // We should have a worksheet + $worksheet = $pDefinedName->getWorksheet() ? $pDefinedName->getWorksheet()->getTitle() : null; + } + } else { + $worksheet = str_replace("''", "'", trim($worksheet, "'")); + } + if (!empty($worksheet)) { + $newRange = "'" . str_replace("'", "''", $worksheet) . "'!"; + } + + if (!empty($column)) { + $newRange .= $column; + } + if (!empty($row)) { + $newRange .= $row; + } + + $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length); + } + + if (substr($definedRange, 0, 1) === '=') { + $definedRange = substr($definedRange, 1); + } + + return $definedRange; + } } From 1272224164ad9f00bed818d2eb5df8224836160b Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 3 Mar 2021 19:12:06 +0100 Subject: [PATCH 21/89] Minor Refactoring --- src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php index 30aa45a3..4c0929db 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/DefinedNames.php @@ -69,7 +69,10 @@ class DefinedNames $this->objWriter->startElement('definedName'); $this->objWriter->writeAttribute('name', $pDefinedName->getName()); if ($pDefinedName->getLocalOnly() && $pDefinedName->getScope() !== null) { - $this->objWriter->writeAttribute('localSheetId', $pDefinedName->getScope()->getParent()->getIndex($pDefinedName->getScope())); + $this->objWriter->writeAttribute( + 'localSheetId', + $pDefinedName->getScope()->getParent()->getIndex($pDefinedName->getScope()) + ); } $definedRange = $this->getDefinedRange($pDefinedName); @@ -207,16 +210,11 @@ class DefinedNames } else { $worksheet = str_replace("''", "'", trim($worksheet, "'")); } + if (!empty($worksheet)) { $newRange = "'" . str_replace("'", "''", $worksheet) . "'!"; } - - if (!empty($column)) { - $newRange .= $column; - } - if (!empty($row)) { - $newRange .= $row; - } + $newRange = "{$newRange}{$column}{$row}"; $definedRange = substr($definedRange, 0, $offset) . $newRange . substr($definedRange, $offset + $length); } From 000e6088c98675882c595e3f8023a49410b34814 Mon Sep 17 00:00:00 2001 From: Patrick Brouwers Date: Wed, 3 Mar 2021 21:34:45 +0100 Subject: [PATCH 22/89] Reverted Scrutinzer fix in Xslx Reader listWorksheetInfo (#1895) --- CHANGELOG.md | 4 ++-- src/PhpSpreadsheet/Reader/Xlsx.php | 3 ++- tests/PhpSpreadsheetTests/Reader/XlsxTest.php | 19 +++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 419869bd..b59bbe9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Nothing. +- Fixed issue with Xlsx@listWorksheetInfo not returning any data ## 1.17.1 - 2021-03-01 @@ -697,4 +697,4 @@ For a comprehensive list of all class changes, and a semi-automated migration pa ## Previous versions of PHPExcel -The changelog for the project when it was called PHPExcel is [still available](./CHANGELOG.PHPExcel.md). \ No newline at end of file +The changelog for the project when it was called PHPExcel is [still available](./CHANGELOG.PHPExcel.md). diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index bf754591..219a49fb 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -187,7 +187,8 @@ class Xlsx extends BaseReader ); if ($xmlWorkbook->sheets) { $dir = dirname($rel['Target']); - foreach ($xmlWorkbook->sheets->sheet->children() as $eleSheet) { + /** @var SimpleXMLElement $eleSheet */ + foreach ($xmlWorkbook->sheets->sheet as $eleSheet) { $tmpInfo = [ 'worksheetName' => (string) $eleSheet['name'], 'lastColumnLetter' => 'A', diff --git a/tests/PhpSpreadsheetTests/Reader/XlsxTest.php b/tests/PhpSpreadsheetTests/Reader/XlsxTest.php index b326c142..cb84a3b7 100644 --- a/tests/PhpSpreadsheetTests/Reader/XlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/XlsxTest.php @@ -55,6 +55,25 @@ class XlsxTest extends TestCase } } + public function testListWorksheetInfo(): void + { + $filename = 'tests/data/Reader/XLSX/rowColumnAttributeTest.xlsx'; + $reader = new Xlsx(); + $actual = $reader->listWorksheetInfo($filename); + + $expected = [ + [ + 'worksheetName' => 'Sheet1', + 'lastColumnLetter' => 'F', + 'lastColumnIndex' => 5, + 'totalRows' => '6', + 'totalColumns' => 6, + ], + ]; + + self::assertEquals($expected, $actual); + } + public function testLoadXlsxRowColumnAttributes(): void { $filename = 'tests/data/Reader/XLSX/rowColumnAttributeTest.xlsx'; From d2a83b404a7a77766d765525e90570be85c7be48 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 3 Mar 2021 23:18:56 +0100 Subject: [PATCH 23/89] Statistical trends additional functions and unit tests (#1896) * PEARSON() and CORREL() are identical functions * Unit tests for GROWTH() function * Move GROWTH() function into Statistical\Trends Class --- .../Calculation/Calculation.php | 4 +-- .../Calculation/Statistical.php | 22 ++++--------- .../Calculation/Statistical/Trends.php | 32 +++++++++++++++++++ .../Functions/Statistical/GrowthTest.php | 32 +++++++++++++++++++ tests/data/Calculation/Statistical/GROWTH.php | 25 +++++++++++++++ 5 files changed, 97 insertions(+), 18 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php create mode 100644 tests/data/Calculation/Statistical/GROWTH.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 7fd07355..04b8ffab 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1206,7 +1206,7 @@ class Calculation ], 'GROWTH' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GROWTH'], + 'functionCall' => [Statistical\Trends::class, 'GROWTH'], 'argumentCount' => '1-4', ], 'HARMEAN' => [ @@ -1897,7 +1897,7 @@ class Calculation ], 'PEARSON' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CORREL'], + 'functionCall' => [Statistical\Trends::class, 'CORREL'], 'argumentCount' => '2', ], 'PERCENTILE' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index b2b9ad50..4b5c8094 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -1544,6 +1544,11 @@ class Statistical * * Returns values along a predicted exponential Trend * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::GROWTH() + * Use the GROWTH() method in the Statistical\Trends class instead + * * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @param mixed[] $newValues Values of X for which we want to find Y @@ -1553,22 +1558,7 @@ class Statistical */ public static function GROWTH($yValues, $xValues = [], $newValues = [], $const = true) { - $yValues = Functions::flattenArray($yValues); - $xValues = Functions::flattenArray($xValues); - $newValues = Functions::flattenArray($newValues); - $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); - - $bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); - if (empty($newValues)) { - $newValues = $bestFitExponential->getXValues(); - } - - $returnArray = []; - foreach ($newValues as $xValue) { - $returnArray[0][] = $bestFitExponential->getValueOfYForX($xValue); - } - - return $returnArray; + return Trends::GROWTH($yValues, $xValues, $newValues, $const); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index 032b4625..23bffced 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -132,6 +132,38 @@ class Trends return $bestFitLinear->getValueOfYForX($xValue); } + /** + * GROWTH. + * + * Returns values along a predicted exponential Trend + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * @param mixed[] $newValues Values of X for which we want to find Y + * @param bool $const a logical value specifying whether to force the intersect to equal 0 + * + * @return array of float + */ + public static function GROWTH($yValues, $xValues = [], $newValues = [], $const = true) + { + $yValues = Functions::flattenArray($yValues); + $xValues = Functions::flattenArray($xValues); + $newValues = Functions::flattenArray($newValues); + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + + $bestFitExponential = Trend::calculate(Trend::TREND_EXPONENTIAL, $yValues, $xValues, $const); + if (empty($newValues)) { + $newValues = $bestFitExponential->getXValues(); + } + + $returnArray = []; + foreach ($newValues as $xValue) { + $returnArray[0][] = [$bestFitExponential->getValueOfYForX($xValue)]; + } + + return $returnArray; + } + /** * INTERCEPT. * diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php new file mode 100644 index 00000000..ce4eef8a --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php @@ -0,0 +1,32 @@ + Date: Thu, 4 Mar 2021 21:45:56 +0100 Subject: [PATCH 24/89] Statistical refactoring - Confidence() and Trend() (#1898) - Move TREND() functions into the Statistical Trends class - Unit tests for TREND() - Create Confidence class for Statistical Confidence functions, and the CONFIDENCE() method --- .../Calculation/Calculation.php | 6 +-- .../Calculation/Statistical.php | 46 ++++++------------- .../Calculation/Statistical/Confidence.php | 41 +++++++++++++++++ .../Calculation/Statistical/Trends.php | 32 +++++++++++++ .../Functions/Statistical/GrowthTest.php | 5 +- .../Functions/Statistical/TrendTest.php | 33 +++++++++++++ tests/PhpSpreadsheetTests/Shared/FontTest.php | 15 +++--- tests/PhpSpreadsheetTests/Style/ColorTest.php | 15 +++--- tests/data/Calculation/Statistical/TREND.php | 34 ++++++++++++++ 9 files changed, 177 insertions(+), 50 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Confidence.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php create mode 100644 tests/data/Calculation/Statistical/TREND.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 04b8ffab..0d06f04e 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -579,12 +579,12 @@ class Calculation ], 'CONFIDENCE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CONFIDENCE'], + 'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'], 'argumentCount' => '3', ], 'CONFIDENCE.NORM' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CONFIDENCE'], + 'functionCall' => [Statistical\Confidence::class, 'CONFIDENCE'], 'argumentCount' => '3', ], 'CONFIDENCE.T' => [ @@ -2454,7 +2454,7 @@ class Calculation ], 'TREND' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'TREND'], + 'functionCall' => [Statistical\Trends::class, 'TREND'], 'argumentCount' => '1-4', ], 'TRIM' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 4b5c8094..8a9e3fea 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Averages; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Conditional; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Confidence; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Counts; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Maximum; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Minimum; @@ -832,6 +833,11 @@ class Statistical * * Returns the confidence interval for a population mean * + * @Deprecated 1.18.0 + * + * @see Statistical\Confidence::CONFIDENCE() + * Use the CONFIDENCE() method in the Statistical\Confidence class instead + * * @param float $alpha * @param float $stdDev Standard Deviation * @param float $size @@ -840,23 +846,7 @@ class Statistical */ public static function CONFIDENCE($alpha, $stdDev, $size) { - $alpha = Functions::flattenSingleValue($alpha); - $stdDev = Functions::flattenSingleValue($stdDev); - $size = Functions::flattenSingleValue($size); - - if ((is_numeric($alpha)) && (is_numeric($stdDev)) && (is_numeric($size))) { - $size = floor($size); - if (($alpha <= 0) || ($alpha >= 1)) { - return Functions::NAN(); - } - if (($stdDev <= 0) || ($size < 1)) { - return Functions::NAN(); - } - - return self::NORMSINV(1 - $alpha / 2) * $stdDev / sqrt($size); - } - - return Functions::VALUE(); + return Confidence::CONFIDENCE($alpha, $stdDev, $size); } /** @@ -2941,6 +2931,11 @@ class Statistical * * Returns values along a linear Trend * + * @Deprecated 1.18.0 + * + * @see Statistical\Trends::TREND() + * Use the TREND() method in the Statistical\Trends class instead + * * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @param mixed[] $newValues Values of X for which we want to find Y @@ -2950,22 +2945,7 @@ class Statistical */ public static function TREND($yValues, $xValues = [], $newValues = [], $const = true) { - $yValues = Functions::flattenArray($yValues); - $xValues = Functions::flattenArray($xValues); - $newValues = Functions::flattenArray($newValues); - $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); - - $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); - if (empty($newValues)) { - $newValues = $bestFitLinear->getXValues(); - } - - $returnArray = []; - foreach ($newValues as $xValue) { - $returnArray[0][] = $bestFitLinear->getValueOfYForX($xValue); - } - - return $returnArray; + return Trends::TREND($yValues, $xValues, $newValues, $const); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php new file mode 100644 index 00000000..c4c2a7dd --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php @@ -0,0 +1,41 @@ += 1)) { + return Functions::NAN(); + } + if (($stdDev <= 0) || ($size < 1)) { + return Functions::NAN(); + } + + return Statistical::NORMSINV(1 - $alpha / 2) * $stdDev / sqrt($size); + } + + return Functions::VALUE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index 23bffced..d06214de 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -388,4 +388,36 @@ class Trends return $bestFitLinear->getStdevOfResiduals(); } + + /** + * TREND. + * + * Returns values along a linear Trend + * + * @param mixed[] $yValues Data Series Y + * @param mixed[] $xValues Data Series X + * @param mixed[] $newValues Values of X for which we want to find Y + * @param bool $const a logical value specifying whether to force the intersect to equal 0 + * + * @return array of float + */ + public static function TREND($yValues, $xValues = [], $newValues = [], $const = true) + { + $yValues = Functions::flattenArray($yValues); + $xValues = Functions::flattenArray($xValues); + $newValues = Functions::flattenArray($newValues); + $const = ($const === null) ? true : (bool) Functions::flattenSingleValue($const); + + $bestFitLinear = Trend::calculate(Trend::TREND_LINEAR, $yValues, $xValues, $const); + if (empty($newValues)) { + $newValues = $bestFitLinear->getXValues(); + } + + $returnArray = []; + foreach ($newValues as $xValue) { + $returnArray[0][] = [$bestFitLinear->getValueOfYForX($xValue)]; + } + + return $returnArray; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php index ce4eef8a..6e1cf7bf 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GrowthTest.php @@ -17,10 +17,11 @@ class GrowthTest extends TestCase * @dataProvider providerGROWTH * * @param mixed $expectedResult + * @param mixed $yValues */ - public function testGROWTH($expectedResult, ...$args): void + public function testGROWTH($expectedResult, $yValues, ...$args): void { - $result = Statistical::GROWTH(...$args); + $result = Statistical::GROWTH($yValues, ...$args); self::assertEqualsWithDelta($expectedResult, $result[0], 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php new file mode 100644 index 00000000..b4f756b5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TrendTest.php @@ -0,0 +1,33 @@ + Date: Sat, 6 Mar 2021 22:50:19 +0100 Subject: [PATCH 25/89] Trend unit tests (#1899) - Move TREND() functions into the Statistical Trends class - Unit tests for TREND() - Create Confidence class for Statistical Confidence functions --- .../Calculation/Statistical/Trends.php | 4 +- src/PhpSpreadsheet/Shared/Trend/BestFit.php | 65 ++++++----- .../Shared/Trend/ExponentialBestFit.php | 21 ++-- .../Shared/Trend/LinearBestFit.php | 5 +- .../Shared/Trend/LogarithmicBestFit.php | 23 ++-- .../Shared/Trend/PolynomialBestFit.php | 3 +- .../Shared/Trend/PowerBestFit.php | 35 +++--- src/PhpSpreadsheet/Shared/Trend/Trend.php | 9 +- .../Functions/Statistical/LogEstTest.php | 2 +- .../Shared/Trend/ExponentialBestFitTest.php | 49 +++++++++ .../Shared/Trend/LinearBestFitTest.php | 49 +++++++++ tests/data/Calculation/Statistical/LINEST.php | 104 ++++++++++++++---- tests/data/Calculation/Statistical/LOGEST.php | 54 +++++++++ .../data/Shared/Trend/ExponentialBestFit.php | 12 ++ tests/data/Shared/Trend/LinearBestFit.php | 20 ++++ 15 files changed, 344 insertions(+), 111 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php create mode 100644 tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php create mode 100644 tests/data/Shared/Trend/ExponentialBestFit.php create mode 100644 tests/data/Shared/Trend/LinearBestFit.php diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index d06214de..a1137cef 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -226,7 +226,7 @@ class Trends ], [ $bestFitLinear->getSlopeSE(), - $bestFitLinear->getIntersectSE(), + ($const === false) ? Functions::NA() : $bestFitLinear->getIntersectSE(), ], [ $bestFitLinear->getGoodnessOfFit(), @@ -293,7 +293,7 @@ class Trends ], [ $bestFitExponential->getSlopeSE(), - $bestFitExponential->getIntersectSE(), + ($const === false) ? Functions::NA() : $bestFitExponential->getIntersectSE(), ], [ $bestFitExponential->getGoodnessOfFit(), diff --git a/src/PhpSpreadsheet/Shared/Trend/BestFit.php b/src/PhpSpreadsheet/Shared/Trend/BestFit.php index c9499722..6d6f6283 100644 --- a/src/PhpSpreadsheet/Shared/Trend/BestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/BestFit.php @@ -348,13 +348,13 @@ class BestFit $bestFitY = $this->yBestFitValues[$xKey] = $this->getValueOfYForX($xValue); $SSres += ($this->yValues[$xKey] - $bestFitY) * ($this->yValues[$xKey] - $bestFitY); - if ($const) { + if ($const === true) { $SStot += ($this->yValues[$xKey] - $meanY) * ($this->yValues[$xKey] - $meanY); } else { $SStot += $this->yValues[$xKey] * $this->yValues[$xKey]; } $SScov += ($this->xValues[$xKey] - $meanX) * ($this->yValues[$xKey] - $meanY); - if ($const) { + if ($const === true) { $SSsex += ($this->xValues[$xKey] - $meanX) * ($this->xValues[$xKey] - $meanX); } else { $SSsex += $this->xValues[$xKey] * $this->xValues[$xKey]; @@ -362,7 +362,7 @@ class BestFit } $this->SSResiduals = $SSres; - $this->DFResiduals = $this->valueCount - 1 - $const; + $this->DFResiduals = $this->valueCount - 1 - ($const === true ? 1 : 0); if ($this->DFResiduals == 0.0) { $this->stdevOfResiduals = 0.0; @@ -395,27 +395,39 @@ class BestFit } } + private function sumSquares(array $values) + { + return array_sum( + array_map( + function ($value) { + return $value ** 2; + }, + $values + ) + ); + } + /** * @param float[] $yValues * @param float[] $xValues - * @param bool $const */ - protected function leastSquareFit(array $yValues, array $xValues, $const): void + protected function leastSquareFit(array $yValues, array $xValues, bool $const): void { // calculate sums - $x_sum = array_sum($xValues); - $y_sum = array_sum($yValues); - $meanX = $x_sum / $this->valueCount; - $meanY = $y_sum / $this->valueCount; - $mBase = $mDivisor = $xx_sum = $xy_sum = $yy_sum = 0.0; + $sumValuesX = array_sum($xValues); + $sumValuesY = array_sum($yValues); + $meanValueX = $sumValuesX / $this->valueCount; + $meanValueY = $sumValuesY / $this->valueCount; + $sumSquaresX = $this->sumSquares($xValues); + $sumSquaresY = $this->sumSquares($yValues); + $mBase = $mDivisor = 0.0; + $xy_sum = 0.0; for ($i = 0; $i < $this->valueCount; ++$i) { $xy_sum += $xValues[$i] * $yValues[$i]; - $xx_sum += $xValues[$i] * $xValues[$i]; - $yy_sum += $yValues[$i] * $yValues[$i]; - if ($const) { - $mBase += ($xValues[$i] - $meanX) * ($yValues[$i] - $meanY); - $mDivisor += ($xValues[$i] - $meanX) * ($xValues[$i] - $meanX); + if ($const === true) { + $mBase += ($xValues[$i] - $meanValueX) * ($yValues[$i] - $meanValueY); + $mDivisor += ($xValues[$i] - $meanValueX) * ($xValues[$i] - $meanValueX); } else { $mBase += $xValues[$i] * $yValues[$i]; $mDivisor += $xValues[$i] * $xValues[$i]; @@ -426,13 +438,9 @@ class BestFit $this->slope = $mBase / $mDivisor; // calculate intersect - if ($const) { - $this->intersect = $meanY - ($this->slope * $meanX); - } else { - $this->intersect = 0; - } + $this->intersect = ($const === true) ? $meanValueY - ($this->slope * $meanValueX) : 0.0; - $this->calculateGoodnessOfFit($x_sum, $y_sum, $xx_sum, $yy_sum, $xy_sum, $meanX, $meanY, $const); + $this->calculateGoodnessOfFit($sumValuesX, $sumValuesY, $sumSquaresX, $sumSquaresY, $xy_sum, $meanValueX, $meanValueY, $const); } /** @@ -440,23 +448,22 @@ class BestFit * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - public function __construct($yValues, $xValues = [], $const = true) + public function __construct($yValues, $xValues = []) { // Calculate number of points - $nY = count($yValues); - $nX = count($xValues); + $yValueCount = count($yValues); + $xValueCount = count($xValues); // Define X Values if necessary - if ($nX == 0) { - $xValues = range(1, $nY); - } elseif ($nY != $nX) { + if ($xValueCount === 0) { + $xValues = range(1, $yValueCount); + } elseif ($yValueCount !== $xValueCount) { // Ensure both arrays of points are the same size $this->error = true; } - $this->valueCount = $nY; + $this->valueCount = $yValueCount; $this->xValues = $xValues; $this->yValues = $yValues; } diff --git a/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php b/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php index 82866dee..eb8cd746 100644 --- a/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/ExponentialBestFit.php @@ -88,20 +88,17 @@ class ExponentialBestFit extends BestFit * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function exponentialRegression($yValues, $xValues, $const): void + private function exponentialRegression(array $yValues, array $xValues, bool $const): void { - foreach ($yValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); - $this->leastSquareFit($yValues, $xValues, $const); + $this->leastSquareFit($adjustedYValues, $xValues, $const); } /** @@ -116,7 +113,7 @@ class ExponentialBestFit extends BestFit parent::__construct($yValues, $xValues); if (!$this->error) { - $this->exponentialRegression($yValues, $xValues, $const); + $this->exponentialRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php b/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php index 26a562c5..65d6b4ff 100644 --- a/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/LinearBestFit.php @@ -56,9 +56,8 @@ class LinearBestFit extends BestFit * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function linearRegression($yValues, $xValues, $const): void + private function linearRegression(array $yValues, array $xValues, bool $const): void { $this->leastSquareFit($yValues, $xValues, $const); } @@ -75,7 +74,7 @@ class LinearBestFit extends BestFit parent::__construct($yValues, $xValues); if (!$this->error) { - $this->linearRegression($yValues, $xValues, $const); + $this->linearRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php b/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php index c469067d..2366dc63 100644 --- a/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/LogarithmicBestFit.php @@ -48,7 +48,7 @@ class LogarithmicBestFit extends BestFit $slope = $this->getSlope($dp); $intersect = $this->getIntersect($dp); - return 'Y = ' . $intersect . ' + ' . $slope . ' * log(X)'; + return 'Y = ' . $slope . ' * log(' . $intersect . ' * X)'; } /** @@ -56,20 +56,17 @@ class LogarithmicBestFit extends BestFit * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function logarithmicRegression($yValues, $xValues, $const): void + private function logarithmicRegression(array $yValues, array $xValues, bool $const): void { - foreach ($xValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); - $this->leastSquareFit($yValues, $xValues, $const); + $this->leastSquareFit($adjustedYValues, $xValues, $const); } /** @@ -84,7 +81,7 @@ class LogarithmicBestFit extends BestFit parent::__construct($yValues, $xValues); if (!$this->error) { - $this->logarithmicRegression($yValues, $xValues, $const); + $this->logarithmicRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php b/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php index d959eddb..1d34e81c 100644 --- a/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/PolynomialBestFit.php @@ -178,9 +178,8 @@ class PolynomialBestFit extends BestFit * @param int $order Order of Polynomial for this regression * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - public function __construct($order, $yValues, $xValues = [], $const = true) + public function __construct($order, $yValues, $xValues = []) { parent::__construct($yValues, $xValues); diff --git a/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php b/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php index c53eab63..cafd0115 100644 --- a/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php +++ b/src/PhpSpreadsheet/Shared/Trend/PowerBestFit.php @@ -72,28 +72,23 @@ class PowerBestFit extends BestFit * * @param float[] $yValues The set of Y-values for this regression * @param float[] $xValues The set of X-values for this regression - * @param bool $const */ - private function powerRegression($yValues, $xValues, $const): void + private function powerRegression(array $yValues, array $xValues, bool $const): void { - foreach ($xValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); - foreach ($yValues as &$value) { - if ($value < 0.0) { - $value = 0 - log(abs($value)); - } elseif ($value > 0.0) { - $value = log($value); - } - } - unset($value); + $adjustedYValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $yValues + ); + $adjustedXValues = array_map( + function ($value) { + return ($value < 0.0) ? 0 - log(abs($value)) : log($value); + }, + $xValues + ); - $this->leastSquareFit($yValues, $xValues, $const); + $this->leastSquareFit($adjustedYValues, $adjustedXValues, $const); } /** @@ -108,7 +103,7 @@ class PowerBestFit extends BestFit parent::__construct($yValues, $xValues); if (!$this->error) { - $this->powerRegression($yValues, $xValues, $const); + $this->powerRegression($yValues, $xValues, (bool) $const); } } } diff --git a/src/PhpSpreadsheet/Shared/Trend/Trend.php b/src/PhpSpreadsheet/Shared/Trend/Trend.php index 1b7b3901..d0a117cb 100644 --- a/src/PhpSpreadsheet/Shared/Trend/Trend.php +++ b/src/PhpSpreadsheet/Shared/Trend/Trend.php @@ -55,10 +55,9 @@ class Trend $nX = count($xValues); // Define X Values if necessary - if ($nX == 0) { + if ($nX === 0) { $xValues = range(1, $nY); - $nX = $nY; - } elseif ($nY != $nX) { + } elseif ($nY !== $nX) { // Ensure both arrays of points are the same size trigger_error('Trend(): Number of elements in coordinate arrays do not match.', E_USER_ERROR); } @@ -84,7 +83,7 @@ class Trend case self::TREND_POLYNOMIAL_6: if (!isset(self::$trendCache[$key])) { $order = substr($trendType, -1); - self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues, $const); + self::$trendCache[$key] = new PolynomialBestFit($order, $yValues, $xValues); } return self::$trendCache[$key]; @@ -100,7 +99,7 @@ class Trend if ($trendType != self::TREND_BEST_FIT_NO_POLY) { foreach (self::$trendTypePolynomialOrders as $trendMethod) { $order = substr($trendMethod, -1); - $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues, $const); + $bestFit[$trendMethod] = new PolynomialBestFit($order, $yValues, $xValues); if ($bestFit[$trendMethod]->getError()) { unset($bestFit[$trendMethod]); } else { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php index 4d926f76..2b2d1ecf 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/LogEstTest.php @@ -19,7 +19,7 @@ class LogEstTest extends TestCase public function testLOGEST($expectedResult, $yValues, $xValues, $const, $stats): void { $result = Statistical::LOGEST($yValues, $xValues, $const, $stats); - + //var_dump($result); $elements = count($expectedResult); for ($element = 0; $element < $elements; ++$element) { self::assertEqualsWithDelta($expectedResult[$element], $result[$element], 1E-12); diff --git a/tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php b/tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php new file mode 100644 index 00000000..32fa9d31 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Trend/ExponentialBestFitTest.php @@ -0,0 +1,49 @@ +getSlope(1); + self::assertEquals($expectedSlope[0], $slope); + $slope = $bestFit->getSlope(); + self::assertEquals($expectedSlope[1], $slope); + $intersect = $bestFit->getIntersect(1); + self::assertEquals($expectedIntersect[0], $intersect); + $intersect = $bestFit->getIntersect(); + self::assertEquals($expectedIntersect[1], $intersect); + + $equation = $bestFit->getEquation(2); + self::assertEquals($expectedEquation, $equation); + + self::assertSame($expectedGoodnessOfFit[0], $bestFit->getGoodnessOfFit(6)); + self::assertSame($expectedGoodnessOfFit[1], $bestFit->getGoodnessOfFit()); + } + + public function providerExponentialBestFit() + { + return require 'tests/data/Shared/Trend/ExponentialBestFit.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php new file mode 100644 index 00000000..02b82038 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Shared/Trend/LinearBestFitTest.php @@ -0,0 +1,49 @@ +getSlope(1); + self::assertEquals($expectedSlope[0], $slope); + $slope = $bestFit->getSlope(); + self::assertEquals($expectedSlope[1], $slope); + $intersect = $bestFit->getIntersect(1); + self::assertEquals($expectedIntersect[0], $intersect); + $intersect = $bestFit->getIntersect(); + self::assertEquals($expectedIntersect[1], $intersect); + + $equation = $bestFit->getEquation(2); + self::assertEquals($expectedEquation, $equation); + + self::assertSame($expectedGoodnessOfFit[0], $bestFit->getGoodnessOfFit(6)); + self::assertSame($expectedGoodnessOfFit[1], $bestFit->getGoodnessOfFit()); + } + + public function providerLinearBestFit() + { + return require 'tests/data/Shared/Trend/LinearBestFit.php'; + } +} diff --git a/tests/data/Calculation/Statistical/LINEST.php b/tests/data/Calculation/Statistical/LINEST.php index 9bd28ffe..ee5fdb0b 100644 --- a/tests/data/Calculation/Statistical/LINEST.php +++ b/tests/data/Calculation/Statistical/LINEST.php @@ -1,6 +1,22 @@ [ + // [ + // [-234.2371645, 2553.21066, 12529.76817, 27.64138737, 52317.83051], + // [13.26801148, 530.6691519, 400.0668382, 5.429374042, 12237.3616], + // [0.996747993, 970.5784629, '#N/A', '#N/A', '#N/A'], + // [459.7536742, 6, '#N/A', '#N/A', '#N/A'], + // [1732393319, 5652135.316, '#N/A', '#N/A', '#N/A'], + // ], + // [142000, 144000, 151000, 150000, 139000, 169000, 126000, 142900, 163000, 169000, 149000], + // [ + // [2310, 2, 2, 20], + // [2333, 2, 2, 12], + // [2356, 3, 1.5, 33], + // [2379, 3, 2, 43], + // [2402, 2, 3, 53], + // [2425, 4, 2, 23], + // [2448, 2, 1.5, 99], + // [2471, 2, 2, 34], + // [2494, 3, 3, 23], + // [2517, 4, 4, 55], + // [2540, 2, 3, 22], + // ], + // true, + // true, // ], - // [142000, 144000, 151000, 150000, 139000, 169000, 126000, 142900, 163000, 169000, 149000], - // [ - // [2310, 2, 2, 20], - // [2333, 2, 2, 12], - // [2356, 3, 1.5, 33], - // [2379, 3, 2, 43], - // [2402, 2, 3, 53], - // [2425, 4, 2, 23], - // [2448, 2, 1.5, 99], - // [2471, 2, 2, 34], - // [2494, 3, 3, 23], - // [2517, 4, 4, 55], - // [2540, 2, 3, 22], - // ], - // true, - // true, - // ], ]; diff --git a/tests/data/Calculation/Statistical/LOGEST.php b/tests/data/Calculation/Statistical/LOGEST.php index be9e4d72..bba7487b 100644 --- a/tests/data/Calculation/Statistical/LOGEST.php +++ b/tests/data/Calculation/Statistical/LOGEST.php @@ -1,6 +1,20 @@ [0.8, 0.813512072856517], + 'intersect' => [20.7, 20.671878197177865], + 'goodnessOfFit' => [0.904868, 0.9048681877346413], + 'equation' => 'Y = 20.67 * 0.81^X', + [3, 10, 3, 6, 8, 12, 1, 4, 9, 14], + [8, 2, 11, 6, 5, 4, 12, 9, 6, 1], + ], +]; diff --git a/tests/data/Shared/Trend/LinearBestFit.php b/tests/data/Shared/Trend/LinearBestFit.php new file mode 100644 index 00000000..b1be2f9a --- /dev/null +++ b/tests/data/Shared/Trend/LinearBestFit.php @@ -0,0 +1,20 @@ + [-1.1, -1.1064189189190], + 'intersect' => [14.1, 14.081081081081], + 'goodnessOfFit' => [0.873138, 0.8731378215564962], + 'equation' => 'Y = 14.08 + -1.11 * X', + [3, 10, 3, 6, 8, 12, 1, 4, 9, 14], + [8, 2, 11, 6, 5, 4, 12, 9, 6, 1], + ], + [ + 'slope' => [1.0, 1.0], + 'intersect' => [-2.0, -2.0], + 'goodnessOfFit' => [1.0, 1.0], + 'equation' => 'Y = -2 + 1 * X', + [1, 2, 3, 4, 5], + [3, 4, 5, 6, 7], + ], +]; From c4ed0ee7b0c646040a4310175362509cdeae40b5 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 7 Mar 2021 14:22:03 +0100 Subject: [PATCH 26/89] Minor scrutinizer improvements (#1906) * Minor scrutinizer improvements * Minor typing improvements --- .../Cell/AddressHelperTest.php | 12 ++++-- .../Cell/CoordinateTest.php | 21 +++++++--- .../Shared/CodePageTest.php | 5 ++- tests/PhpSpreadsheetTests/Shared/DateTest.php | 41 +++++++++++++------ 4 files changed, 55 insertions(+), 24 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php b/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php index 9b21767b..ffe0e05f 100644 --- a/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AddressHelperTest.php @@ -26,9 +26,13 @@ class AddressHelperTest extends TestCase /** * @dataProvider providerR1C1ConversionToA1Relative */ - public function testR1C1ConversionToA1Relative(string $expectedValue, string $address, ?int $row = null, ?int $column = null): void - { - $args = [$address]; + public function testR1C1ConversionToA1Relative( + string $expectedValue, + string $address, + ?int $row = null, + ?int $column = null + ): void { + $args = []; if ($row !== null) { $args[] = $row; } @@ -36,7 +40,7 @@ class AddressHelperTest extends TestCase $args[] = $column; } - $actualValue = AddressHelper::convertToA1(...$args); + $actualValue = AddressHelper::convertToA1($address, ...$args); self::assertSame($expectedValue, $actualValue); } diff --git a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php index 8e0e98a9..159af3b9 100644 --- a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php @@ -232,10 +232,11 @@ class CoordinateTest extends TestCase * @dataProvider providerBuildRange * * @param mixed $expectedResult + * @param mixed $rangeSets */ - public function testBuildRange($expectedResult, ...$args): void + public function testBuildRange($expectedResult, $rangeSets): void { - $result = Coordinate::buildRange(...$args); + $result = Coordinate::buildRange($rangeSets); self::assertEquals($expectedResult, $result); } @@ -248,7 +249,16 @@ class CoordinateTest extends TestCase { $this->expectException(TypeError::class); - $cellRange = ''; + $cellRange = null; + Coordinate::buildRange($cellRange); + } + + public function testBuildRangeInvalid2(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Range does not contain any information'); + + $cellRange = []; Coordinate::buildRange($cellRange); } @@ -342,10 +352,11 @@ class CoordinateTest extends TestCase * @dataProvider providerMergeRangesInCollection * * @param mixed $expectedResult + * @param mixed $rangeSets */ - public function testMergeRangesInCollection($expectedResult, ...$args): void + public function testMergeRangesInCollection($expectedResult, $rangeSets): void { - $result = Coordinate::mergeRangesInCollection(...$args); + $result = Coordinate::mergeRangesInCollection($rangeSets); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Shared/CodePageTest.php b/tests/PhpSpreadsheetTests/Shared/CodePageTest.php index 2bdbda72..eb121889 100644 --- a/tests/PhpSpreadsheetTests/Shared/CodePageTest.php +++ b/tests/PhpSpreadsheetTests/Shared/CodePageTest.php @@ -12,10 +12,11 @@ class CodePageTest extends TestCase * @dataProvider providerCodePage * * @param mixed $expectedResult + * @param mixed $codePageIndex */ - public function testCodePageNumberToName($expectedResult, ...$args): void + public function testCodePageNumberToName($expectedResult, $codePageIndex): void { - $result = CodePage::numberToName(...$args); + $result = CodePage::numberToName($codePageIndex); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Shared/DateTest.php b/tests/PhpSpreadsheetTests/Shared/DateTest.php index 550e2f6a..4ab7461b 100644 --- a/tests/PhpSpreadsheetTests/Shared/DateTest.php +++ b/tests/PhpSpreadsheetTests/Shared/DateTest.php @@ -39,7 +39,7 @@ class DateTest extends TestCase public function testSetExcelCalendarWithInvalidValue(): void { - $unsupportedCalendar = '2012'; + $unsupportedCalendar = 2012; $result = Date::setExcelCalendar($unsupportedCalendar); self::assertFalse($result); } @@ -48,15 +48,16 @@ class DateTest extends TestCase * @dataProvider providerDateTimeExcelToTimestamp1900 * * @param mixed $expectedResult + * @param mixed $excelDateTimeValue */ - public function testDateTimeExcelToTimestamp1900($expectedResult, ...$args): void + public function testDateTimeExcelToTimestamp1900($expectedResult, $excelDateTimeValue): void { if (is_numeric($expectedResult) && ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN)) { self::markTestSkipped('Test invalid on 32-bit system.'); } Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - $result = Date::excelToTimestamp(...$args); + $result = Date::excelToTimestamp($excelDateTimeValue); self::assertEquals($expectedResult, $result); } @@ -69,12 +70,13 @@ class DateTest extends TestCase * @dataProvider providerDateTimeTimestampToExcel1900 * * @param mixed $expectedResult + * @param mixed $unixTimestamp */ - public function testDateTimeTimestampToExcel1900($expectedResult, ...$args): void + public function testDateTimeTimestampToExcel1900($expectedResult, $unixTimestamp): void { Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - $result = Date::timestampToExcel(...$args); + $result = Date::timestampToExcel($unixTimestamp); self::assertEqualsWithDelta($expectedResult, $result, 1E-5); } @@ -87,12 +89,13 @@ class DateTest extends TestCase * @dataProvider providerDateTimeDateTimeToExcel * * @param mixed $expectedResult + * @param mixed $dateTimeObject */ - public function testDateTimeDateTimeToExcel($expectedResult, ...$args): void + public function testDateTimeDateTimeToExcel($expectedResult, $dateTimeObject): void { Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - $result = Date::dateTimeToExcel(...$args); + $result = Date::dateTimeToExcel($dateTimeObject); self::assertEqualsWithDelta($expectedResult, $result, 1E-5); } @@ -123,15 +126,16 @@ class DateTest extends TestCase * @dataProvider providerDateTimeExcelToTimestamp1904 * * @param mixed $expectedResult + * @param mixed $excelDateTimeValue */ - public function testDateTimeExcelToTimestamp1904($expectedResult, ...$args): void + public function testDateTimeExcelToTimestamp1904($expectedResult, $excelDateTimeValue): void { if (is_numeric($expectedResult) && ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN)) { self::markTestSkipped('Test invalid on 32-bit system.'); } Date::setExcelCalendar(Date::CALENDAR_MAC_1904); - $result = Date::excelToTimestamp(...$args); + $result = Date::excelToTimestamp($excelDateTimeValue); self::assertEquals($expectedResult, $result); } @@ -144,12 +148,13 @@ class DateTest extends TestCase * @dataProvider providerDateTimeTimestampToExcel1904 * * @param mixed $expectedResult + * @param mixed $unixTimestamp */ - public function testDateTimeTimestampToExcel1904($expectedResult, ...$args): void + public function testDateTimeTimestampToExcel1904($expectedResult, $unixTimestamp): void { Date::setExcelCalendar(Date::CALENDAR_MAC_1904); - $result = Date::timestampToExcel(...$args); + $result = Date::timestampToExcel($unixTimestamp); self::assertEqualsWithDelta($expectedResult, $result, 1E-5); } @@ -178,15 +183,17 @@ class DateTest extends TestCase * @dataProvider providerDateTimeExcelToTimestamp1900Timezone * * @param mixed $expectedResult + * @param mixed $excelDateTimeValue + * @param mixed $timezone */ - public function testDateTimeExcelToTimestamp1900Timezone($expectedResult, ...$args): void + public function testDateTimeExcelToTimestamp1900Timezone($expectedResult, $excelDateTimeValue, $timezone): void { if (is_numeric($expectedResult) && ($expectedResult > PHP_INT_MAX || $expectedResult < PHP_INT_MIN)) { self::markTestSkipped('Test invalid on 32-bit system.'); } Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - $result = Date::excelToTimestamp(...$args); + $result = Date::excelToTimestamp($excelDateTimeValue, $timezone); self::assertEquals($expectedResult, $result); } @@ -202,29 +209,37 @@ class DateTest extends TestCase self::assertTrue((bool) Date::stringToExcel('2019-02-28')); self::assertTrue((bool) Date::stringToExcel('2019-02-28 11:18')); self::assertFalse(Date::stringToExcel('2019-02-28 11:71')); + $date = Date::PHPToExcel('2020-01-01'); self::assertEquals(43831.0, $date); + $spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setCellValue('B1', 'x'); $val = $sheet->getCell('B1')->getValue(); self::assertFalse(Date::timestampToExcel($val)); + $cell = $sheet->getCell('A1'); self::assertNotNull($cell); + $cell->setValue($date); $sheet->getStyle('A1') ->getNumberFormat() ->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME); self::assertTrue(null !== $cell && Date::isDateTime($cell)); + $cella2 = $sheet->getCell('A2'); self::assertNotNull($cella2); + $cella2->setValue('=A1+2'); $sheet->getStyle('A2') ->getNumberFormat() ->setFormatCode(NumberFormat::FORMAT_DATE_DATETIME); self::assertTrue(null !== $cella2 && Date::isDateTime($cella2)); + $cella3 = $sheet->getCell('A3'); self::assertNotNull($cella3); + $cella3->setValue('=A1+4'); $sheet->getStyle('A3') ->getNumberFormat() From f81ffd9a4fe7b173c385c180812b6b3a3e3aa1b8 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 8 Mar 2021 12:54:06 +0100 Subject: [PATCH 27/89] Additional argument validation for LEFT(), MID() and RIGHT() text functions (#1909) * Additional argument validation for LEFT(), MID() and RIGHT() text functions --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Calculation/TextData.php | 10 +++------- tests/data/Calculation/TextData/LEFT.php | 10 ++++++++++ tests/data/Calculation/TextData/MID.php | 14 +++++++++++++- tests/data/Calculation/TextData/RIGHT.php | 10 ++++++++++ 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b59bbe9d..2474e26d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed - Fixed issue with Xlsx@listWorksheetInfo not returning any data +- Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions. [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640) ## 1.17.1 - 2021-03-01 diff --git a/src/PhpSpreadsheet/Calculation/TextData.php b/src/PhpSpreadsheet/Calculation/TextData.php index cea15fdc..b886ce08 100644 --- a/src/PhpSpreadsheet/Calculation/TextData.php +++ b/src/PhpSpreadsheet/Calculation/TextData.php @@ -299,7 +299,7 @@ class TextData $value = Functions::flattenSingleValue($value); $chars = Functions::flattenSingleValue($chars); - if ($chars < 0) { + if (!is_numeric($chars) || $chars < 0) { return Functions::VALUE(); } @@ -325,7 +325,7 @@ class TextData $start = Functions::flattenSingleValue($start); $chars = Functions::flattenSingleValue($chars); - if (($start < 1) || ($chars < 0)) { + if (!is_numeric($start) || $start < 1 || !is_numeric($chars) || $chars < 0) { return Functions::VALUE(); } @@ -333,10 +333,6 @@ class TextData $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); } - if (empty($chars)) { - return ''; - } - return mb_substr($value, --$start, $chars, 'UTF-8'); } @@ -353,7 +349,7 @@ class TextData $value = Functions::flattenSingleValue($value); $chars = Functions::flattenSingleValue($chars); - if ($chars < 0) { + if (!is_numeric($chars) || $chars < 0) { return Functions::VALUE(); } diff --git a/tests/data/Calculation/TextData/LEFT.php b/tests/data/Calculation/TextData/LEFT.php index 914d16be..96702f6b 100644 --- a/tests/data/Calculation/TextData/LEFT.php +++ b/tests/data/Calculation/TextData/LEFT.php @@ -16,6 +16,16 @@ return [ 'QWERTYUIOP', -1, ], + [ + '#VALUE!', + 'QWERTYUIOP', + 'NaN', + ], + [ + '#VALUE!', + 'QWERTYUIOP', + null, + ], [ 'ABC', 'ABCDEFGHI', diff --git a/tests/data/Calculation/TextData/MID.php b/tests/data/Calculation/TextData/MID.php index 2a59373b..71d90e8b 100644 --- a/tests/data/Calculation/TextData/MID.php +++ b/tests/data/Calculation/TextData/MID.php @@ -26,7 +26,19 @@ return [ -1, ], [ - '', + '#VALUE!', + 'QWERTYUIOP', + 'NaN', + 1, + ], + [ + '#VALUE!', + 'QWERTYUIOP', + 2, + 'NaN', + ], + [ + '#VALUE!', 'QWERTYUIOP', 5, ], diff --git a/tests/data/Calculation/TextData/RIGHT.php b/tests/data/Calculation/TextData/RIGHT.php index 78ea6a69..95dfe96e 100644 --- a/tests/data/Calculation/TextData/RIGHT.php +++ b/tests/data/Calculation/TextData/RIGHT.php @@ -16,6 +16,16 @@ return [ 'QWERTYUIOP', -1, ], + [ + '#VALUE!', + 'QWERTYUIOP', + 'NaN', + ], + [ + '#VALUE!', + 'QWERTYUIOP', + null, + ], [ 'GHI', 'ABCDEFGHI', From 70f372d88c9c6bea223bfb9c791c5c78fbbbbd26 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 10 Mar 2021 21:18:33 +0100 Subject: [PATCH 28/89] Start refactoring the Lookup and Reference functions (#1912) * Start refactoring the Lookup and Reference functions - COLUMN(), COLUMNS(), ROW() and ROWS() - LOOKUP(), VLOOKUP() and HLOOKUP() - Refactor TRANSPOSE() and ADDRESS() functions into their own classes * Additional unit tests - LOOKUP() - TRANSPOSE() - ADDRESS() --- .../Calculation/Calculation.php | 20 +- src/PhpSpreadsheet/Calculation/LookupRef.php | 433 ++++-------------- .../Calculation/LookupRef/Address.php | 97 ++++ .../Calculation/LookupRef/HLookup.php | 86 ++++ .../Calculation/LookupRef/Lookup.php | 105 +++++ .../Calculation/LookupRef/LookupBase.php | 48 ++ .../Calculation/LookupRef/Matrix.php | 33 ++ .../LookupRef/RowColumnInformation.php | 172 +++++++ .../Calculation/LookupRef/VLookup.php | 105 +++++ .../Functions/LookupRef/AddressTest.php | 31 ++ .../Functions/LookupRef/ColumnTest.php | 17 +- .../Functions/LookupRef/RowTest.php | 17 +- .../Functions/LookupRef/TransposeTest.php | 32 ++ .../Cell/DefaultValueBinderTest.php | 5 +- tests/data/Calculation/LookupRef/ADDRESS.php | 56 +++ tests/data/Calculation/LookupRef/COLUMN.php | 20 + tests/data/Calculation/LookupRef/HLOOKUP.php | 20 + tests/data/Calculation/LookupRef/LOOKUP.php | 42 +- tests/data/Calculation/LookupRef/ROW.php | 16 + .../data/Calculation/LookupRef/TRANSPOSE.php | 32 ++ tests/data/Calculation/LookupRef/VLOOKUP.php | 29 +- 21 files changed, 1041 insertions(+), 375 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Address.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/TransposeTest.php create mode 100644 tests/data/Calculation/LookupRef/ADDRESS.php create mode 100644 tests/data/Calculation/LookupRef/TRANSPOSE.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 0d06f04e..3adcf243 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -263,7 +263,7 @@ class Calculation ], 'ADDRESS' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'cellAddress'], + 'functionCall' => [LookupRef\Address::class, 'cell'], 'argumentCount' => '2-5', ], 'AGGREGATE' => [ @@ -543,13 +543,14 @@ class Calculation ], 'COLUMN' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'COLUMN'], + 'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMN'], 'argumentCount' => '-1', + 'passCellReference' => true, 'passByReference' => [true], ], 'COLUMNS' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'COLUMNS'], + 'functionCall' => [LookupRef\RowColumnInformation::class, 'COLUMNS'], 'argumentCount' => '1', ], 'COMBIN' => [ @@ -1231,7 +1232,7 @@ class Calculation ], 'HLOOKUP' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'HLOOKUP'], + 'functionCall' => [LookupRef\HLookup::class, 'lookup'], 'argumentCount' => '3,4', ], 'HOUR' => [ @@ -1605,7 +1606,7 @@ class Calculation ], 'LOOKUP' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'LOOKUP'], + 'functionCall' => [LookupRef\Lookup::class, 'lookup'], 'argumentCount' => '2,3', ], 'LOWER' => [ @@ -2127,13 +2128,14 @@ class Calculation ], 'ROW' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'ROW'], + 'functionCall' => [LookupRef\RowColumnInformation::class, 'ROW'], 'argumentCount' => '-1', + 'passCellReference' => true, 'passByReference' => [true], ], 'ROWS' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'ROWS'], + 'functionCall' => [LookupRef\RowColumnInformation::class, 'ROWS'], 'argumentCount' => '1', ], 'RRI' => [ @@ -2449,7 +2451,7 @@ class Calculation ], 'TRANSPOSE' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'TRANSPOSE'], + 'functionCall' => [LookupRef\Matrix::class, 'transpose'], 'argumentCount' => '1', ], 'TREND' => [ @@ -2559,7 +2561,7 @@ class Calculation ], 'VLOOKUP' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'VLOOKUP'], + 'functionCall' => [LookupRef\VLookup::class, 'lookup'], 'argumentCount' => '3,4', ], 'WEBSERVICE' => [ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 45aa9239..39823a20 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -2,6 +2,12 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Address; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\HLookup; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Lookup; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\RowColumnInformation; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\VLookup; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; @@ -17,15 +23,19 @@ class LookupRef * Excel Function: * =ADDRESS(row, column, [relativity], [referenceStyle], [sheetText]) * + * @Deprecated 1.18.0 + * + * @see Use the cell() method in the LookupRef\Address class instead + * * @param mixed $row Row number to use in the cell reference * @param mixed $column Column number to use in the cell reference * @param int $relativity Flag indicating the type of reference to return * 1 or omitted Absolute - * 2 Absolute row; relative column - * 3 Relative row; absolute column - * 4 Relative + * 2 Absolute row; relative column + * 3 Relative row; absolute column + * 4 Relative * @param bool $referenceStyle A logical value that specifies the A1 or R1C1 reference style. - * TRUE or omitted CELL_ADDRESS returns an A1-style reference + * 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 * @@ -33,87 +43,33 @@ class LookupRef */ public static function cellAddress($row, $column, $relativity = 1, $referenceStyle = true, $sheetText = '') { - $row = Functions::flattenSingleValue($row); - $column = Functions::flattenSingleValue($column); - $relativity = Functions::flattenSingleValue($relativity); - $sheetText = Functions::flattenSingleValue($sheetText); - - if (($row < 1) || ($column < 1)) { - return Functions::VALUE(); - } - - if ($sheetText > '') { - if (strpos($sheetText, ' ') !== false) { - $sheetText = "'" . $sheetText . "'"; - } - $sheetText .= '!'; - } - if ((!is_bool($referenceStyle)) || $referenceStyle) { - $rowRelative = $columnRelative = '$'; - $column = Coordinate::stringFromColumnIndex($column); - if (($relativity == 2) || ($relativity == 4)) { - $columnRelative = ''; - } - if (($relativity == 3) || ($relativity == 4)) { - $rowRelative = ''; - } - - return $sheetText . $columnRelative . $column . $rowRelative . $row; - } - if (($relativity == 2) || ($relativity == 4)) { - $column = '[' . $column . ']'; - } - if (($relativity == 3) || ($relativity == 4)) { - $row = '[' . $row . ']'; - } - - return $sheetText . 'R' . $row . 'C' . $column; + return Address::cell($row, $column, $relativity, $referenceStyle, $sheetText); } /** * COLUMN. * * Returns the column number of the given cell reference - * If the cell reference is a range of cells, COLUMN returns the column numbers of each column in the reference as a horizontal array. - * If cell reference is omitted, and the function is being called through the calculation engine, then it is assumed to be the - * reference of the cell in which the COLUMN function appears; otherwise this function returns 0. + * If the cell reference is a range of cells, COLUMN returns the column numbers of each column + * in the reference as a horizontal array. + * If cell reference is omitted, and the function is being called through the calculation engine, + * then it is assumed to be the reference of the cell in which the COLUMN function appears; + * otherwise this function returns 1. * * Excel Function: * =COLUMN([cellAddress]) * + * @Deprecated 1.18.0 + * + * @see Use the COLUMN() method in the LookupRef\RowColumnInformation class instead + * * @param null|array|string $cellAddress A reference to a range of cells for which you want the column numbers * * @return int|int[] */ - public static function COLUMN($cellAddress = null) + public static function COLUMN($cellAddress = null, ?Cell $cell = null) { - if ($cellAddress === null || trim($cellAddress) === '') { - return 0; - } - - if (is_array($cellAddress)) { - foreach ($cellAddress as $columnKey => $value) { - $columnKey = preg_replace('/[^a-z]/i', '', $columnKey); - - return (int) Coordinate::columnIndexFromString($columnKey); - } - } else { - [$sheet, $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); - $returnValue = []; - do { - $returnValue[] = (int) Coordinate::columnIndexFromString($startAddress); - } while ($startAddress++ != $endAddress); - - return $returnValue; - } - $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); - - return (int) Coordinate::columnIndexFromString($cellAddress); - } + return RowColumnInformation::COLUMN($cellAddress, $cell); } /** @@ -124,73 +80,44 @@ class LookupRef * Excel Function: * =COLUMNS(cellAddress) * - * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells for which you want the number of columns + * @Deprecated 1.18.0 + * + * @see Use the COLUMNS() method in the LookupRef\RowColumnInformation class instead + * + * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells + * for which you want the number of columns * * @return int|string The number of columns in cellAddress, or a string if arguments are invalid */ public static function COLUMNS($cellAddress = null) { - if ($cellAddress === null || $cellAddress === '') { - return 1; - } elseif (!is_array($cellAddress)) { - return Functions::VALUE(); - } - - reset($cellAddress); - $isMatrix = (is_numeric(key($cellAddress))); - [$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); - - if ($isMatrix) { - return $rows; - } - - return $columns; + return RowColumnInformation::COLUMNS($cellAddress); } /** * ROW. * * Returns the row number of the given cell reference - * If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference as a vertical array. - * If cell reference is omitted, and the function is being called through the calculation engine, then it is assumed to be the - * reference of the cell in which the ROW function appears; otherwise this function returns 0. + * If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference + * as a vertical array. + * If cell reference is omitted, and the function is being called through the calculation engine, + * then it is assumed to be the reference of the cell in which the ROW function appears; + * otherwise this function returns 1. * * Excel Function: * =ROW([cellAddress]) * + * @Deprecated 1.18.0 + * + * @see Use the ROW() method in the LookupRef\RowColumnInformation class instead + * * @param null|array|string $cellAddress A reference to a range of cells for which you want the row numbers * * @return int|mixed[]|string */ - public static function ROW($cellAddress = null) + public static function ROW($cellAddress = null, ?Cell $cell = null) { - if ($cellAddress === null || trim($cellAddress) === '') { - return 0; - } - - if (is_array($cellAddress)) { - foreach ($cellAddress as $columnKey => $rowValue) { - foreach ($rowValue as $rowKey => $cellValue) { - return (int) preg_replace('/\D/', '', $rowKey); - } - } - } else { - [$sheet, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - if (strpos($cellAddress, ':') !== false) { - [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/\D/', '', $startAddress); - $endAddress = preg_replace('/\D/', '', $endAddress); - $returnValue = []; - do { - $returnValue[][] = (int) $startAddress; - } while ($startAddress++ != $endAddress); - - return $returnValue; - } - [$cellAddress] = explode(':', $cellAddress); - - return (int) preg_replace('/\D/', '', $cellAddress); - } + return RowColumnInformation::ROW($cellAddress, $cell); } /** @@ -201,27 +128,18 @@ class LookupRef * Excel Function: * =ROWS(cellAddress) * - * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells for which you want the number of rows + * @Deprecated 1.18.0 + * + * @see Use the ROWS() method in the LookupRef\RowColumnInformation class instead + * + * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells + * for which you want the number of rows * * @return int|string The number of rows in cellAddress, or a string if arguments are invalid */ public static function ROWS($cellAddress = null) { - if ($cellAddress === null || $cellAddress === '') { - return 1; - } elseif (!is_array($cellAddress)) { - return Functions::VALUE(); - } - - reset($cellAddress); - $isMatrix = (is_numeric(key($cellAddress))); - [$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); - - if ($isMatrix) { - return $columns; - } - - return $rows; + return RowColumnInformation::ROWS($cellAddress); } /** @@ -669,204 +587,74 @@ class LookupRef /** * TRANSPOSE. * + * @Deprecated 1.18.0 + * + * @see Use the transpose() method in the LookupRef\Matrix class instead + * * @param array $matrixData A matrix of values * * @return array * - * Unlike the Excel TRANSPOSE function, which will only work on a single row or column, this function will transpose a full matrix + * Unlike the Excel TRANSPOSE function, which will only work on a single row or column, + * this function will transpose a full matrix */ public static function TRANSPOSE($matrixData) { - $returnMatrix = []; - if (!is_array($matrixData)) { - $matrixData = [[$matrixData]]; - } - - $column = 0; - foreach ($matrixData as $matrixRow) { - $row = 0; - foreach ($matrixRow as $matrixCell) { - $returnMatrix[$row][$column] = $matrixCell; - ++$row; - } - ++$column; - } - - return $returnMatrix; - } - - private static function vlookupSort($a, $b) - { - reset($a); - $firstColumn = key($a); - $aLower = StringHelper::strToLower($a[$firstColumn]); - $bLower = StringHelper::strToLower($b[$firstColumn]); - if ($aLower == $bLower) { - return 0; - } - - return ($aLower < $bLower) ? -1 : 1; + return Matrix::transpose($matrixData); } /** * VLOOKUP - * The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value in the same row based on the index_number. + * The VLOOKUP function searches for value in the left-most column of lookup_array and returns the value + * in the same row based on the index_number. + * + * @Deprecated 1.18.0 + * + * @see Use the lookup() method in the LookupRef\VLookup class instead * * @param mixed $lookup_value The value that you want to match in lookup_array * @param mixed $lookup_array The range of cells being searched - * @param mixed $index_number The column number in table_array from which the matching value must be returned. The first column is 1. + * @param mixed $index_number The column number in table_array from which the matching value must be returned. + * The first column is 1. * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value * * @return mixed The value of the found cell */ public static function VLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true) { - $lookup_value = Functions::flattenSingleValue($lookup_value); - $index_number = Functions::flattenSingleValue($index_number); - $not_exact_match = Functions::flattenSingleValue($not_exact_match); - - // index_number must be greater than or equal to 1 - if ($index_number < 1) { - return Functions::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))) { - return Functions::REF(); - } - $f = array_keys($lookup_array); - $firstRow = array_pop($f); - if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array[$firstRow]))) { - return Functions::REF(); - } - $columnKeys = array_keys($lookup_array[$firstRow]); - $returnColumn = $columnKeys[--$index_number]; - $firstColumn = array_shift($columnKeys); - - if (!$not_exact_match) { - uasort($lookup_array, ['self', 'vlookupSort']); - } - - $lookupLower = StringHelper::strToLower($lookup_value); - $rowNumber = $rowValue = false; - foreach ($lookup_array as $rowKey => $rowData) { - $firstLower = StringHelper::strToLower($rowData[$firstColumn]); - - // break if we have passed possible keys - if ( - (is_numeric($lookup_value) && is_numeric($rowData[$firstColumn]) && ($rowData[$firstColumn] > $lookup_value)) || - (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn]) && ($firstLower > $lookupLower)) - ) { - break; - } - // remember the last key, but only if datatypes match - if ( - (is_numeric($lookup_value) && is_numeric($rowData[$firstColumn])) || - (!is_numeric($lookup_value) && !is_numeric($rowData[$firstColumn])) - ) { - if ($not_exact_match) { - $rowNumber = $rowKey; - - continue; - } elseif ( - ($firstLower == $lookupLower) - // Spreadsheets software returns first exact match, - // we have sorted and we might have broken key orders - // we want the first one (by its initial index) - && (($rowNumber == false) || ($rowKey < $rowNumber)) - ) { - $rowNumber = $rowKey; - } - } - } - - if ($rowNumber !== false) { - // return the appropriate value - return $lookup_array[$rowNumber][$returnColumn]; - } - - return Functions::NA(); + return VLookup::lookup($lookup_value, $lookup_array, $index_number, $not_exact_match); } /** * HLOOKUP - * The HLOOKUP function searches for value in the top-most row of lookup_array and returns the value in the same column based on the index_number. + * The HLOOKUP function searches for value in the top-most row of lookup_array and returns the value + * in the same column based on the index_number. + * + * @Deprecated 1.18.0 + * + * @see Use the lookup() method in the LookupRef\HLookup class instead * * @param mixed $lookup_value The value that you want to match in lookup_array * @param mixed $lookup_array The range of cells being searched - * @param mixed $index_number The row number in table_array from which the matching value must be returned. The first row is 1. + * @param mixed $index_number The row number in table_array from which the matching value must be returned. + * The first row is 1. * @param mixed $not_exact_match determines if you are looking for an exact match based on lookup_value * * @return mixed The value of the found cell */ public static function HLOOKUP($lookup_value, $lookup_array, $index_number, $not_exact_match = true) { - $lookup_value = Functions::flattenSingleValue($lookup_value); - $index_number = Functions::flattenSingleValue($index_number); - $not_exact_match = Functions::flattenSingleValue($not_exact_match); - - // index_number must be greater than or equal to 1 - if ($index_number < 1) { - return Functions::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))) { - return Functions::REF(); - } - $f = array_keys($lookup_array); - $firstRow = reset($f); - if ((!is_array($lookup_array[$firstRow])) || ($index_number > count($lookup_array))) { - return Functions::REF(); - } - - $firstkey = $f[0] - 1; - $returnColumn = $firstkey + $index_number; - $firstColumn = array_shift($f); - $rowNumber = null; - foreach ($lookup_array[$firstColumn] as $rowKey => $rowData) { - // break if we have passed possible keys - $bothNumeric = is_numeric($lookup_value) && is_numeric($rowData); - $bothNotNumeric = !is_numeric($lookup_value) && !is_numeric($rowData); - $lookupLower = StringHelper::strToLower($lookup_value); - $rowDataLower = StringHelper::strToLower($rowData); - - if ( - $not_exact_match && ( - ($bothNumeric && $rowData > $lookup_value) || - ($bothNotNumeric && $rowDataLower > $lookupLower) - ) - ) { - break; - } - - // Remember the last key, but only if datatypes match (as in VLOOKUP) - if ($bothNumeric || $bothNotNumeric) { - if ($not_exact_match) { - $rowNumber = $rowKey; - - continue; - } elseif ( - $rowDataLower === $lookupLower - && ($rowNumber === null || $rowKey < $rowNumber) - ) { - $rowNumber = $rowKey; - } - } - } - - if ($rowNumber !== null) { - // otherwise return the appropriate value - return $lookup_array[$returnColumn][$rowNumber]; - } - - return Functions::NA(); + return HLookup::lookup($lookup_value, $lookup_array, $index_number, $not_exact_match); } /** * LOOKUP * The LOOKUP function searches for value either from a one-row or one-column range or from an array. * + * @Deprecated 1.18.0 + * + * @see Use the lookup() method in the LookupRef\Lookup class instead + * * @param mixed $lookup_value The value that you want to match in lookup_array * @param mixed $lookup_vector The range of cells being searched * @param null|mixed $result_vector The column from which the matching value must be returned @@ -875,66 +663,7 @@ class LookupRef */ public static function LOOKUP($lookup_value, $lookup_vector, $result_vector = null) { - $lookup_value = Functions::flattenSingleValue($lookup_value); - - if (!is_array($lookup_vector)) { - return Functions::NA(); - } - $hasResultVector = isset($result_vector); - $lookupRows = count($lookup_vector); - $l = array_keys($lookup_vector); - $l = array_shift($l); - $lookupColumns = count($lookup_vector[$l]); - // we correctly orient our results - if (($lookupRows === 1 && $lookupColumns > 1) || (!$hasResultVector && $lookupRows === 2 && $lookupColumns !== 2)) { - $lookup_vector = self::TRANSPOSE($lookup_vector); - $lookupRows = count($lookup_vector); - $l = array_keys($lookup_vector); - $lookupColumns = count($lookup_vector[array_shift($l)]); - } - - if ($result_vector === null) { - $result_vector = $lookup_vector; - } - $resultRows = count($result_vector); - $l = array_keys($result_vector); - $l = array_shift($l); - $resultColumns = count($result_vector[$l]); - // we correctly orient our results - if ($resultRows === 1 && $resultColumns > 1) { - $result_vector = self::TRANSPOSE($result_vector); - $resultRows = count($result_vector); - $r = array_keys($result_vector); - $resultColumns = count($result_vector[array_shift($r)]); - } - - if ($lookupRows === 2 && !$hasResultVector) { - $result_vector = array_pop($lookup_vector); - $lookup_vector = array_shift($lookup_vector); - } - - if ($lookupColumns !== 2) { - foreach ($lookup_vector as &$value) { - if (is_array($value)) { - $k = array_keys($value); - $key1 = $key2 = array_shift($k); - ++$key2; - $dataValue1 = $value[$key1]; - } else { - $key1 = 0; - $key2 = 1; - $dataValue1 = $value; - } - $dataValue2 = array_shift($result_vector); - if (is_array($dataValue2)) { - $dataValue2 = array_shift($dataValue2); - } - $value = [$key1 => $dataValue1, $key2 => $dataValue2]; - } - unset($value); - } - - return self::VLOOKUP($lookup_value, $lookup_vector, 2); + return Lookup::lookup($lookup_value, $lookup_vector, $result_vector); } /** diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php new file mode 100644 index 00000000..53c9c9d8 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -0,0 +1,97 @@ + '') { + if (strpos($sheetName, ' ') !== false || strpos($sheetName, '[') !== false) { + $sheetName = "'{$sheetName}'"; + } + $sheetName .= '!'; + } + + return $sheetName; + } + + private static function formatAsA1(int $row, int $column, int $relativity, string $sheetName): string + { + $rowRelative = $columnRelative = '$'; + if (($relativity == self::ADDRESS_COLUMN_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $columnRelative = ''; + } + if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $rowRelative = ''; + } + $column = Coordinate::stringFromColumnIndex($column); + + return "{$sheetName}{$columnRelative}{$column}{$rowRelative}{$row}"; + } + + private static function formatAsR1C1(int $row, int $column, int $relativity, string $sheetName): string + { + if (($relativity == self::ADDRESS_COLUMN_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $column = "[{$column}]"; + } + if (($relativity == self::ADDRESS_ROW_RELATIVE) || ($relativity == self::ADDRESS_RELATIVE)) { + $row = "[{$row}]"; + } + + return "{$sheetName}R{$row}C{$column}"; + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php new file mode 100644 index 00000000..559fe7d1 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/HLookup.php @@ -0,0 +1,86 @@ +getMessage(); + } + + $f = array_keys($lookupArray); + $firstRow = reset($f); + if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray))) { + return Functions::REF(); + } + + $firstkey = $f[0] - 1; + $returnColumn = $firstkey + $indexNumber; + $firstColumn = array_shift($f); + $rowNumber = self::hLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); + + if ($rowNumber !== null) { + // otherwise return the appropriate value + return $lookupArray[$returnColumn][$rowNumber]; + } + + return Functions::NA(); + } + + private static function hLookupSearch($lookupValue, $lookupArray, $column, $notExactMatch) + { + $lookupLower = StringHelper::strToLower($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); + + if ( + $notExactMatch && + (($bothNumeric && $rowData > $lookupValue) || ($bothNotNumeric && $cellDataLower > $lookupLower)) + ) { + break; + } + + $rowNumber = self::checkMatch( + $bothNumeric, + $bothNotNumeric, + $notExactMatch, + $rowKey, + $cellDataLower, + $lookupLower, + $rowNumber + ); + } + + return $rowNumber; + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php new file mode 100644 index 00000000..9d75efb0 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php @@ -0,0 +1,105 @@ + 1) || (!$hasResultVector && $lookupRows === 2 && $lookupColumns !== 2)) { + $lookupVector = LookupRef::TRANSPOSE($lookupVector); + $lookupRows = self::rowCount($lookupVector); + $lookupColumns = self::columnCount($lookupVector); + } + + $resultVector = self::verifyResultVector($lookupVector, $resultVector); + + if ($lookupRows === 2 && !$hasResultVector) { + $resultVector = array_pop($lookupVector); + $lookupVector = array_shift($lookupVector); + } + + if ($lookupColumns !== 2) { + $lookupVector = self::verifyLookupValues($lookupVector, $resultVector); + } + + return VLookup::lookup($lookupValue, $lookupVector, 2); + } + + private static function verifyLookupValues(array $lookupVector, array $resultVector): array + { + foreach ($lookupVector as &$value) { + if (is_array($value)) { + $k = array_keys($value); + $key1 = $key2 = array_shift($k); + ++$key2; + $dataValue1 = $value[$key1]; + } else { + $key1 = 0; + $key2 = 1; + $dataValue1 = $value; + } + + $dataValue2 = array_shift($resultVector); + if (is_array($dataValue2)) { + $dataValue2 = array_shift($dataValue2); + } + $value = [$key1 => $dataValue1, $key2 => $dataValue2]; + } + unset($value); + + return $lookupVector; + } + + private static function verifyResultVector(array $lookupVector, $resultVector) + { + if ($resultVector === null) { + $resultVector = $lookupVector; + } + + $resultRows = self::rowCount($resultVector); + $resultColumns = self::columnCount($resultVector); + + // we correctly orient our results + if ($resultRows === 1 && $resultColumns > 1) { + $resultVector = LookupRef::TRANSPOSE($resultVector); + } + + return $resultVector; + } + + private static function rowCount(array $dataArray): int + { + return count($dataArray); + } + + private static function columnCount(array $dataArray): int + { + $rowKeys = array_keys($dataArray); + $row = array_shift($rowKeys); + + return count($dataArray[$row]); + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php new file mode 100644 index 00000000..80fc99ad --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/LookupBase.php @@ -0,0 +1,48 @@ +getColumn()) : 1; + } + + if (is_array($cellAddress)) { + foreach ($cellAddress as $columnKey => $value) { + $columnKey = preg_replace('/[^a-z]/i', '', $columnKey); + + return (int) Coordinate::columnIndexFromString($columnKey); + } + } else { + [, $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); + $returnValue = []; + do { + $returnValue[] = (int) Coordinate::columnIndexFromString($startAddress); + } while ($startAddress++ != $endAddress); + + return $returnValue; + } + $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); + + return (int) Coordinate::columnIndexFromString($cellAddress); + } + } + + /** + * COLUMNS. + * + * Returns the number of columns in an array or reference. + * + * Excel Function: + * =COLUMNS(cellAddress) + * + * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells + * for which you want the number of columns + * + * @return int|string The number of columns in cellAddress, or a string if arguments are invalid + */ + public static function COLUMNS($cellAddress = null) + { + if ($cellAddress === null || $cellAddress === '') { + return 1; + } elseif (!is_array($cellAddress)) { + return Functions::VALUE(); + } + + reset($cellAddress); + $isMatrix = (is_numeric(key($cellAddress))); + [$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); + + if ($isMatrix) { + return $rows; + } + + return $columns; + } + + /** + * ROW. + * + * Returns the row number of the given cell reference + * If the cell reference is a range of cells, ROW returns the row numbers of each row in the reference + * as a vertical array. + * If cell reference is omitted, and the function is being called through the calculation engine, + * then it is assumed to be the reference of the cell in which the ROW function appears; + * otherwise this function returns 1. + * + * Excel Function: + * =ROW([cellAddress]) + * + * @param null|array|string $cellAddress A reference to a range of cells for which you want the row numbers + * + * @return int|mixed[]|string + */ + public static function ROW($cellAddress = null, ?Cell $pCell = null) + { + if ($cellAddress === null || (!is_array($cellAddress) && trim($cellAddress) === '')) { + return ($pCell !== null) ? $pCell->getRow() : 1; + } + + if (is_array($cellAddress)) { + foreach ($cellAddress as $columnKey => $rowValue) { + foreach ($rowValue as $rowKey => $cellValue) { + return (int) preg_replace('/\D/', '', $rowKey); + } + } + } else { + [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + if (strpos($cellAddress, ':') !== false) { + [$startAddress, $endAddress] = explode(':', $cellAddress); + $startAddress = preg_replace('/\D/', '', $startAddress); + $endAddress = preg_replace('/\D/', '', $endAddress); + $returnValue = []; + do { + $returnValue[][] = (int) $startAddress; + } while ($startAddress++ != $endAddress); + + return $returnValue; + } + [$cellAddress] = explode(':', $cellAddress); + + return (int) preg_replace('/\D/', '', $cellAddress); + } + } + + /** + * ROWS. + * + * Returns the number of rows in an array or reference. + * + * Excel Function: + * =ROWS(cellAddress) + * + * @param null|array|string $cellAddress An array or array formula, or a reference to a range of cells + * for which you want the number of rows + * + * @return int|string The number of rows in cellAddress, or a string if arguments are invalid + */ + public static function ROWS($cellAddress = null) + { + if ($cellAddress === null || $cellAddress === '') { + return 1; + } elseif (!is_array($cellAddress)) { + return Functions::VALUE(); + } + + reset($cellAddress); + $isMatrix = (is_numeric(key($cellAddress))); + [$columns, $rows] = Calculation::getMatrixDimensions($cellAddress); + + if ($isMatrix) { + return $columns; + } + + return $rows; + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php new file mode 100644 index 00000000..f890e496 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/VLookup.php @@ -0,0 +1,105 @@ +getMessage(); + } + + $f = array_keys($lookupArray); + $firstRow = array_pop($f); + if ((!is_array($lookupArray[$firstRow])) || ($indexNumber > count($lookupArray[$firstRow]))) { + return Functions::REF(); + } + $columnKeys = array_keys($lookupArray[$firstRow]); + $returnColumn = $columnKeys[--$indexNumber]; + $firstColumn = array_shift($columnKeys); + + if (!$notExactMatch) { + uasort($lookupArray, ['self', 'vlookupSort']); + } + + $rowNumber = self::vLookupSearch($lookupValue, $lookupArray, $firstColumn, $notExactMatch); + + if ($rowNumber !== null) { + // return the appropriate value + return $lookupArray[$rowNumber][$returnColumn]; + } + + return Functions::NA(); + } + + private static function vlookupSort($a, $b) + { + reset($a); + $firstColumn = key($a); + $aLower = StringHelper::strToLower($a[$firstColumn]); + $bLower = StringHelper::strToLower($b[$firstColumn]); + + if ($aLower == $bLower) { + return 0; + } + + return ($aLower < $bLower) ? -1 : 1; + } + + private static function vLookupSearch($lookupValue, $lookupArray, $column, $notExactMatch) + { + $lookupLower = StringHelper::strToLower($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]); + + // break if we have passed possible keys + if ( + $notExactMatch && + (($bothNumeric && ($rowData[$column] > $lookupValue)) || + ($bothNotNumeric && ($cellDataLower > $lookupLower))) + ) { + break; + } + + $rowNumber = self::checkMatch( + $bothNumeric, + $bothNotNumeric, + $notExactMatch, + $rowKey, + $cellDataLower, + $lookupLower, + $rowNumber + ); + } + + return $rowNumber; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php new file mode 100644 index 00000000..17063edc --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AddressTest.php @@ -0,0 +1,31 @@ +getMockBuilder(Cell::class) + ->setMethods(['getColumn']) + ->disableOriginalConstructor() + ->getMock(); + $cell->method('getColumn') + ->willReturn('D'); + + $result = LookupRef::COLUMN(null, $cell); + self::assertSame(4, $result); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php index 9471e647..804b924d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Cell\Cell; use PHPUnit\Framework\TestCase; class RowTest extends TestCase @@ -17,8 +18,9 @@ class RowTest extends TestCase * @dataProvider providerROW * * @param mixed $expectedResult + * @param null|mixed $cellReference */ - public function testROW($expectedResult, string $cellReference): void + public function testROW($expectedResult, $cellReference = null): void { $result = LookupRef::ROW($cellReference); self::assertSame($expectedResult, $result); @@ -28,4 +30,17 @@ class RowTest extends TestCase { return require 'tests/data/Calculation/LookupRef/ROW.php'; } + + public function testROWwithNull(): void + { + $cell = $this->getMockBuilder(Cell::class) + ->setMethods(['getRow']) + ->disableOriginalConstructor() + ->getMock(); + $cell->method('getRow') + ->willReturn(3); + + $result = LookupRef::ROW(null, $cell); + self::assertSame(3, $result); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/TransposeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/TransposeTest.php new file mode 100644 index 00000000..1c75ab09 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/TransposeTest.php @@ -0,0 +1,32 @@ + 'B5', 'C' => 'C5', 'D' => 'D5'], + ], + [ + [2, 3, 4], + 'B2:D3', + ], + [ + [2, 3, 4], + 'Sheet1!B2:D2', + ], + [ + [2, 3, 4], + '"WorkSheet #1"!B2:D2', + ], ]; diff --git a/tests/data/Calculation/LookupRef/HLOOKUP.php b/tests/data/Calculation/LookupRef/HLOOKUP.php index b880f247..d2a8a446 100644 --- a/tests/data/Calculation/LookupRef/HLOOKUP.php +++ b/tests/data/Calculation/LookupRef/HLOOKUP.php @@ -308,4 +308,24 @@ return [ 2, false, ], + [ + '#VALUE!', + 'B', + [ + ['Selection column', 'C', 'B', 'A'], + ['Value to retrieve', 3, 2, 1], + ], + 'Nan', + false, + ], + [ + '#REF!', + 'B', + [ + 'Selection column', + 'Value to retrieve', + ], + 2, + false, + ], ]; diff --git a/tests/data/Calculation/LookupRef/LOOKUP.php b/tests/data/Calculation/LookupRef/LOOKUP.php index ab322d57..9c7d96eb 100644 --- a/tests/data/Calculation/LookupRef/LOOKUP.php +++ b/tests/data/Calculation/LookupRef/LOOKUP.php @@ -1,7 +1,6 @@ Date: Wed, 10 Mar 2021 12:23:08 -0800 Subject: [PATCH 29/89] Fix for Issue #1887 - Lose Track of Selected Cells After Save (#1908) * Fix for Issue #1887 - Lose Track of Selected Cells After Save Issue #1887 reports that selected cells are lost after saving Xlsx. Testing indicates that this applies to the object in memory, though not to the saved spreadsheet. Xlsx writer tries to save calculated values for cells which contain formulas. Calculation::_calculateFormulaValue issues a getStyle call merely to retrieve the quotePrefix property, which, if set, indicates that the cell does not contain a formula even though it looks like one. A side-effect of calls to getStyle is that selectedCell is updated. That is clearly accidental, and highly undesirable, in this case. Code is changed to save selectedCell before getStyle call and restore it afterwards. The problem was reported only for Xlsx save. To be on the safe side, test is made for output formats of Xlsx, Xls, Ods, Html (which basically includes Pdf), and Csv. For all of those, the object in memory is tested after the save. For Xlsx and Xls, the saved file is also tested. It does not make sense to test the saved file for Csv and Html. It does make sense to test it for Ods, but the necessary support is not yet present in either the Ods Reader or Ods Writer - a project for another day. * Move Logic Out of Calculation, Add Support for Ods ActiveSheet and SelectedCells Mark Baker thought logic belonged in Worksheet, not Calculation. I couldn't get it to work in Worksheet, but doing it in Cell works, and that has already been used to preserve ActiveSheet over call to getCalculatedValue, so this just extends that idea to SelectedCells. Original tests could not completely support Ods because of a lack of support for ActiveSheet and SelectedCells in Ods Reader and Writer. There's a lot missing in Ods support, but a journey of 1000 miles ... Those two particular concepts are now supported for Ods. --- src/PhpSpreadsheet/Cell/Cell.php | 2 + src/PhpSpreadsheet/Reader/Ods.php | 72 +++++++++++++++++ src/PhpSpreadsheet/Writer/Ods/Settings.php | 51 ++++++++++-- .../Calculation/CalculationTest.php | 9 +++ .../Reader/Ods/OdsTest.php | 14 ++++ .../Writer/RetainSelectedCellsTest.php | 77 +++++++++++++++++++ 6 files changed, 219 insertions(+), 6 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Writer/RetainSelectedCellsTest.php diff --git a/src/PhpSpreadsheet/Cell/Cell.php b/src/PhpSpreadsheet/Cell/Cell.php index 5dee411b..f971a3c8 100644 --- a/src/PhpSpreadsheet/Cell/Cell.php +++ b/src/PhpSpreadsheet/Cell/Cell.php @@ -252,9 +252,11 @@ class Cell if ($this->dataType == DataType::TYPE_FORMULA) { try { $index = $this->getWorksheet()->getParent()->getActiveSheetIndex(); + $selected = $this->getWorksheet()->getSelectedCells(); $result = Calculation::getInstance( $this->getWorksheet()->getParent() )->calculateCellValue($this, $resetLog); + $this->getWorksheet()->setSelectedCells($selected); $this->getWorksheet()->getParent()->setActiveSheetIndex($index); // We don't yet handle array returns if (is_array($result)) { diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 4ceac653..59d934be 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -23,6 +23,7 @@ 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; @@ -646,10 +647,81 @@ class Ods extends BaseReader $this->readDefinedExpressions($spreadsheet, $workbookData, $tableNs); } $spreadsheet->setActiveSheetIndex(0); + + if ($zip->locateName('settings.xml') !== false) { + $this->processSettings($zip, $spreadsheet); + } // Return return $spreadsheet; } + private function processSettings(ZipArchive $zip, Spreadsheet $spreadsheet): void + { + $dom = new DOMDocument('1.01', 'UTF-8'); + $dom->loadXML( + $this->securityScanner->scan($zip->getFromName('settings.xml')), + Settings::getLibXmlLoaderOptions() + ); + //$xlinkNs = $dom->lookupNamespaceUri('xlink'); + $configNs = $dom->lookupNamespaceUri('config'); + //$oooNs = $dom->lookupNamespaceUri('ooo'); + $officeNs = $dom->lookupNamespaceUri('office'); + $settings = $dom->getElementsByTagNameNS($officeNs, 'settings') + ->item(0); + $this->lookForActiveSheet($settings, $spreadsheet, $configNs); + $this->lookForSelectedCells($settings, $spreadsheet, $configNs); + } + + private function lookForActiveSheet(DOMNode $settings, Spreadsheet $spreadsheet, string $configNs): void + { + foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) { + if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') { + try { + $spreadsheet->setActiveSheetIndexByName($t->nodeValue); + } catch (Throwable $e) { + // do nothing + } + + break; + } + } + } + + private function lookForSelectedCells(DOMNode $settings, Spreadsheet $spreadsheet, string $configNs): void + { + foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) { + if ($t->getAttributeNs($configNs, 'name') === 'Tables') { + foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) { + $setRow = $setCol = ''; + $wsname = $ws->getAttributeNs($configNs, 'name'); + foreach ($ws->getElementsByTagNameNS($configNs, 'config-item') as $configItem) { + $attrName = $configItem->getAttributeNs($configNs, 'name'); + if ($attrName === 'CursorPositionX') { + $setCol = $configItem->nodeValue; + } + if ($attrName === 'CursorPositionY') { + $setRow = $configItem->nodeValue; + } + } + $this->setSelected($spreadsheet, $wsname, $setCol, $setRow); + } + + break; + } + } + } + + private function setSelected(Spreadsheet $spreadsheet, string $wsname, string $setCol, string $setRow): void + { + if (is_numeric($setCol) && is_numeric($setRow)) { + try { + $spreadsheet->getSheetByName($wsname)->setSelectedCellByColumnAndRow($setCol + 1, $setRow + 1); + } catch (Throwable $e) { + // do nothing + } + } + } + /** * Recursively scan element. * diff --git a/src/PhpSpreadsheet/Writer/Ods/Settings.php b/src/PhpSpreadsheet/Writer/Ods/Settings.php index d458e8c2..301daf03 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Settings.php +++ b/src/PhpSpreadsheet/Writer/Ods/Settings.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; +use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -16,7 +17,6 @@ class Settings extends WriterPart */ public function write(?Spreadsheet $spreadsheet = null) { - $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); } else { @@ -39,13 +39,52 @@ class Settings extends WriterPart $objWriter->writeAttribute('config:name', 'ooo:view-settings'); $objWriter->startElement('config:config-item-map-indexed'); $objWriter->writeAttribute('config:name', 'Views'); - $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->startElement('config:config-item-map-entry'); + $spreadsheet = $spreadsheet ?? $this->getParentWriter()->getSpreadsheet(); + + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'ViewId'); + $objWriter->writeAttribute('config:type', 'string'); + $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 + $wstitle = $spreadsheet->getActiveSheet()->getTitle(); + $objWriter->startElement('config:config-item'); + $objWriter->writeAttribute('config:name', 'ActiveTable'); + $objWriter->writeAttribute('config:type', 'string'); + $objWriter->text($wstitle); + $objWriter->endElement(); // config:config-item ActiveTable + + $objWriter->endElement(); // config:config-item-map-entry + $objWriter->endElement(); // config:config-item-map-indexed Views + $objWriter->endElement(); // config:config-item-set ooo:view-settings $objWriter->startElement('config:config-item-set'); $objWriter->writeAttribute('config:name', 'ooo:configuration-settings'); - $objWriter->endElement(); - $objWriter->endElement(); - $objWriter->endElement(); + $objWriter->endElement(); // config:config-item-set ooo:configuration-settings + $objWriter->endElement(); // office:settings + $objWriter->endElement(); // office:document-settings return $objWriter->getData(); } diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php index 8e339207..337501f9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -159,6 +160,14 @@ class CalculationTest extends TestCase $cell->getStyle()->setQuotePrefix(true); self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue()); + + $cell2 = $workSheet->getCell('A2'); + $cell2->setValueExplicit('ABC', DataType::TYPE_FORMULA); + self::assertEquals('ABC', $cell2->getCalculatedValue()); + + $cell3 = $workSheet->getCell('A3'); + $cell3->setValueExplicit('=', DataType::TYPE_FORMULA); + self::assertEquals('', $cell3->getCalculatedValue()); } public function testCellWithDdeExpresion(): void diff --git a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php index 0160f68d..2cc5377a 100644 --- a/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Ods/OdsTest.php @@ -101,6 +101,20 @@ class OdsTest extends TestCase self::assertEquals('Sheet1', $spreadsheet->getSheet(0)->getTitle()); } + public function testLoadOneWorksheetNotActive(): void + { + $filename = 'tests/data/Reader/Ods/data.ods'; + + // Load into this instance + $reader = new Ods(); + $reader->setLoadSheetsOnly(['Second Sheet']); + $spreadsheet = $reader->load($filename); + + self::assertEquals(1, $spreadsheet->getSheetCount()); + + self::assertEquals('Second Sheet', $spreadsheet->getSheet(0)->getTitle()); + } + public function testLoadBadFile(): void { $this->expectException(ReaderException::class); diff --git a/tests/PhpSpreadsheetTests/Writer/RetainSelectedCellsTest.php b/tests/PhpSpreadsheetTests/Writer/RetainSelectedCellsTest.php new file mode 100644 index 00000000..c1a57eb5 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Writer/RetainSelectedCellsTest.php @@ -0,0 +1,77 @@ +getActiveSheet(); + $sheet->setCellValue('A1', '=SIN(1)') + ->setCellValue('A2', '=SIN(2)') + ->setCellValue('A3', '=SIN(3)') + ->setCellValue('B1', '=SIN(4)') + ->setCellValue('B2', '=SIN(5)') + ->setCellValue('B3', '=SIN(6)') + ->setCellValue('C1', '=SIN(7)') + ->setCellValue('C2', '=SIN(8)') + ->setCellValue('C3', '=SIN(9)'); + $sheet->setSelectedCell('A3'); + $sheet = $spreadsheet->createSheet(); + $sheet->setCellValue('A1', '=SIN(1)') + ->setCellValue('A2', '=SIN(2)') + ->setCellValue('A3', '=SIN(3)') + ->setCellValue('B1', '=SIN(4)') + ->setCellValue('B2', '=SIN(5)') + ->setCellValue('B3', '=SIN(6)') + ->setCellValue('C1', '=SIN(7)') + ->setCellValue('C2', '=SIN(8)') + ->setCellValue('C3', '=SIN(9)'); + $sheet->setSelectedCell('B1'); + $sheet = $spreadsheet->createSheet(); + $sheet->setCellValue('A1', '=SIN(1)') + ->setCellValue('A2', '=SIN(2)') + ->setCellValue('A3', '=SIN(3)') + ->setCellValue('B1', '=SIN(4)') + ->setCellValue('B2', '=SIN(5)') + ->setCellValue('B3', '=SIN(6)') + ->setCellValue('C1', '=SIN(7)') + ->setCellValue('C2', '=SIN(8)') + ->setCellValue('C3', '=SIN(9)'); + $sheet->setSelectedCell('C2'); + $spreadsheet->setActiveSheetIndex(1); + + $reloaded = $this->writeAndReload($spreadsheet, $format); + self::assertEquals('A3', $spreadsheet->getSheet(0)->getSelectedCells()); + self::assertEquals('B1', $spreadsheet->getSheet(1)->getSelectedCells()); + self::assertEquals('C2', $spreadsheet->getSheet(2)->getSelectedCells()); + self::assertEquals(1, $spreadsheet->getActiveSheetIndex()); + // SelectedCells and ActiveSheet don't make sense for Html, Csv. + if ($format === 'Xlsx' || $format === 'Xls' || $format === 'Ods') { + self::assertEquals('A3', $reloaded->getSheet(0)->getSelectedCells()); + self::assertEquals('B1', $reloaded->getSheet(1)->getSelectedCells()); + self::assertEquals('C2', $reloaded->getSheet(2)->getSelectedCells()); + self::assertEquals(1, $reloaded->getActiveSheetIndex()); + } + } +} From 4717741dc24b2cc294568169316984c8377c728e Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 10 Mar 2021 21:30:39 +0100 Subject: [PATCH 30/89] Update ChangeLog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2474e26d..b5e273f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Nothing. +- Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908) ### Changed @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fix for [Issue #1887](https://github.com/PHPOffice/PhpSpreadsheet/issues/1887) - Lose Track of Selected Cells After Save - Fixed issue with Xlsx@listWorksheetInfo not returning any data - Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions. [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640) From 499ce61cf7aa496e504e0ab9aa185d602843f572 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 10 Mar 2021 22:38:41 +0100 Subject: [PATCH 31/89] Unhappy path tests for FORMULATEXT() Function (#1915) * Unhappy path tests --- tests/PhpSpreadsheetTests/Calculation/LookupRefTest.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/PhpSpreadsheetTests/Calculation/LookupRefTest.php b/tests/PhpSpreadsheetTests/Calculation/LookupRefTest.php index 04dc0a32..6d603722 100644 --- a/tests/PhpSpreadsheetTests/Calculation/LookupRefTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/LookupRefTest.php @@ -73,4 +73,10 @@ class LookupRefTest extends TestCase { return require 'tests/data/Calculation/LookupRef/FORMULATEXT.php'; } + + public function testFormulaTextWithoutCell(): void + { + $result = LookupRef::FORMULATEXT('A1'); + self::assertEquals(Functions::REF(), $result); + } } From 2259de578b5717d0b7e3d59ba9de227d6e80c7b6 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Thu, 11 Mar 2021 22:34:47 +0100 Subject: [PATCH 32/89] Lookup ref further tests and examples (#1918) * Extract LookupRef\INDEX() into index() method of LookupRef\Matrix class Additional tests * Bugfix for returning a column using INDEX() * Some improvements to ROW() and COLUMN() * Simplify some of the INDEX() logic, eliminating redundant code --- samples/Calculations/LookupRef/ADDRESS.php | 22 ++++++ samples/Calculations/LookupRef/COLUMN.php | 23 ++++++ samples/Calculations/LookupRef/COLUMNS.php | 21 ++++++ samples/Calculations/LookupRef/INDEX.php | 39 ++++++++++ samples/Calculations/LookupRef/ROW.php | 20 +++++ samples/Calculations/LookupRef/ROWS.php | 20 +++++ src/PhpSpreadsheet/Calculation/LookupRef.php | 60 +++------------ .../Calculation/LookupRef/Matrix.php | 71 ++++++++++++++++++ .../LookupRef/RowColumnInformation.php | 73 ++++++++++--------- .../Functions/LookupRef/IndexTest.php | 1 + tests/data/Calculation/LookupRef/INDEX.php | 72 +++++++++++++++++- tests/data/Calculation/LookupRef/ROW.php | 7 ++ 12 files changed, 343 insertions(+), 86 deletions(-) create mode 100644 samples/Calculations/LookupRef/ADDRESS.php create mode 100644 samples/Calculations/LookupRef/COLUMN.php create mode 100644 samples/Calculations/LookupRef/COLUMNS.php create mode 100644 samples/Calculations/LookupRef/INDEX.php create mode 100644 samples/Calculations/LookupRef/ROW.php create mode 100644 samples/Calculations/LookupRef/ROWS.php diff --git a/samples/Calculations/LookupRef/ADDRESS.php b/samples/Calculations/LookupRef/ADDRESS.php new file mode 100644 index 00000000..2377541d --- /dev/null +++ b/samples/Calculations/LookupRef/ADDRESS.php @@ -0,0 +1,22 @@ +log('Returns a text reference to a single cell in a worksheet.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->getCell('A1')->setValue('=ADDRESS(2,3)'); +$worksheet->getCell('A2')->setValue('=ADDRESS(2,3,2)'); +$worksheet->getCell('A3')->setValue('=ADDRESS(2,3,2,FALSE)'); +$worksheet->getCell('A4')->setValue('=ADDRESS(2,3,1,FALSE,"[Book1]Sheet1")'); +$worksheet->getCell('A5')->setValue('=ADDRESS(2,3,1,FALSE,"EXCEL SHEET")'); + +for ($row = 1; $row <= 5; ++$row) { + $cell = $worksheet->getCell("A{$row}"); + $helper->log("A{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/samples/Calculations/LookupRef/COLUMN.php b/samples/Calculations/LookupRef/COLUMN.php new file mode 100644 index 00000000..e9e58466 --- /dev/null +++ b/samples/Calculations/LookupRef/COLUMN.php @@ -0,0 +1,23 @@ +log('Returns the column index of a cell.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->getCell('A1')->setValue('=COLUMN(C13)'); +$worksheet->getCell('A2')->setValue('=COLUMN(E13:G15)'); +$worksheet->getCell('F1')->setValue('=COLUMN()'); + +for ($row = 1; $row <= 2; ++$row) { + $cell = $worksheet->getCell("A{$row}"); + $helper->log("A{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} + +$cell = $worksheet->getCell('F1'); +$helper->log("F1: {$cell->getValue()} => {$cell->getCalculatedValue()}"); diff --git a/samples/Calculations/LookupRef/COLUMNS.php b/samples/Calculations/LookupRef/COLUMNS.php new file mode 100644 index 00000000..4d7f8d10 --- /dev/null +++ b/samples/Calculations/LookupRef/COLUMNS.php @@ -0,0 +1,21 @@ +log('Returns the number of columns in an array or reference.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->getCell('A1')->setValue('=COLUMNS(C1:G4)'); +$worksheet->getCell('A2')->setValue('=COLUMNS({1,2,3;4,5,6})'); +$worksheet->getCell('A3')->setValue('=ROWS(C1:E4 D3:G5)'); +$worksheet->getCell('A4')->setValue('=COLUMNS(1:1)'); + +for ($row = 1; $row <= 4; ++$row) { + $cell = $worksheet->getCell("A{$row}"); + $helper->log("A{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/samples/Calculations/LookupRef/INDEX.php b/samples/Calculations/LookupRef/INDEX.php new file mode 100644 index 00000000..9ef0b945 --- /dev/null +++ b/samples/Calculations/LookupRef/INDEX.php @@ -0,0 +1,39 @@ +log('Returns the row index of a cell.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$data1 = [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], +]; + +$data2 = [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], +]; + +$worksheet->fromArray($data1, null, 'A1'); +$worksheet->fromArray($data2, null, 'C1'); + +$worksheet->getCell('A11')->setValue('=INDEX(A1:B2, 2, 2)'); +$worksheet->getCell('A12')->setValue('=INDEX(A1:B2, 2, 1)'); +$worksheet->getCell('A13')->setValue('=INDEX({1,2;3,4}, 0, 2)'); +$worksheet->getCell('A14')->setValue('=INDEX(C1:C5, 5)'); +$worksheet->getCell('A15')->setValue('=INDEX(C1:D5, 5, 2)'); +$worksheet->getCell('A16')->setValue('=SUM(INDEX(C1:D5, 5, 0))'); + +for ($row = 11; $row <= 16; ++$row) { + $cell = $worksheet->getCell("A{$row}"); + $helper->log("A{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/samples/Calculations/LookupRef/ROW.php b/samples/Calculations/LookupRef/ROW.php new file mode 100644 index 00000000..560639a5 --- /dev/null +++ b/samples/Calculations/LookupRef/ROW.php @@ -0,0 +1,20 @@ +log('Returns the row index of a cell.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->getCell('A1')->setValue('=ROW(C13)'); +$worksheet->getCell('A2')->setValue('=ROW(E19:G21)'); +$worksheet->getCell('A3')->setValue('=ROW()'); + +for ($row = 1; $row <= 3; ++$row) { + $cell = $worksheet->getCell("A{$row}"); + $helper->log("A{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/samples/Calculations/LookupRef/ROWS.php b/samples/Calculations/LookupRef/ROWS.php new file mode 100644 index 00000000..3cdf085b --- /dev/null +++ b/samples/Calculations/LookupRef/ROWS.php @@ -0,0 +1,20 @@ +log('Returns the row index of a cell.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$worksheet->getCell('A1')->setValue('=ROWS(C1:E4)'); +$worksheet->getCell('A2')->setValue('=ROWS({1,2,3;4,5,6})'); +$worksheet->getCell('A3')->setValue('=ROWS(C1:E4 D3:G5)'); + +for ($row = 1; $row <= 3; ++$row) { + $cell = $worksheet->getCell("A{$row}"); + $helper->log("A{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 39823a20..f1a147f1 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -529,59 +529,21 @@ class LookupRef * Excel Function: * =INDEX(range_array, row_num, [column_num]) * - * @param mixed $arrayValues A range of cells or an array constant - * @param mixed $rowNum The row in array from which to return a value. If row_num is omitted, column_num is required. - * @param mixed $columnNum The column in array from which to return a value. If column_num is omitted, row_num is required. + * @Deprecated 1.18.0 + * + * @see Use the index() method in the LookupRef\Matrix class instead + * + * @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. + * @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. + * @param mixed $matrix * * @return mixed the value of a specified cell or array of cells */ - public static function INDEX($arrayValues, $rowNum = 0, $columnNum = 0) + public static function INDEX($matrix, $rowNum = 0, $columnNum = 0) { - $rowNum = Functions::flattenSingleValue($rowNum); - $columnNum = Functions::flattenSingleValue($columnNum); - - if (($rowNum < 0) || ($columnNum < 0)) { - return Functions::VALUE(); - } - - if (!is_array($arrayValues) || ($rowNum > count($arrayValues))) { - return Functions::REF(); - } - - $rowKeys = array_keys($arrayValues); - $columnKeys = @array_keys($arrayValues[$rowKeys[0]]); - - if ($columnNum > count($columnKeys)) { - return Functions::VALUE(); - } elseif ($columnNum == 0) { - if ($rowNum == 0) { - return $arrayValues; - } - $rowNum = $rowKeys[--$rowNum]; - $returnArray = []; - foreach ($arrayValues as $arrayColumn) { - if (is_array($arrayColumn)) { - if (isset($arrayColumn[$rowNum])) { - $returnArray[] = $arrayColumn[$rowNum]; - } else { - return [$rowNum => $arrayValues[$rowNum]]; - } - } else { - return $arrayValues[$rowNum]; - } - } - - return $returnArray; - } - $columnNum = $columnKeys[--$columnNum]; - if ($rowNum > count($rowKeys)) { - return Functions::VALUE(); - } elseif ($rowNum == 0) { - return $arrayValues[$columnNum]; - } - $rowNum = $rowKeys[--$rowNum]; - - return $arrayValues[$rowNum][$columnNum]; + return Matrix::index($matrix, $rowNum, $columnNum); } /** diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index 99fd6e6e..8859a287 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; + class Matrix { /** @@ -30,4 +32,73 @@ class Matrix return $returnMatrix; } + + /** + * INDEX. + * + * Uses an index to choose a value from a reference or array + * + * Excel Function: + * =INDEX(range_array, row_num, [column_num]) + * + * @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. + * @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. + * + * @return mixed the value of a specified cell or array of cells + */ + public static function index($matrix, $rowNum = 0, $columnNum = 0) + { + $rowNum = Functions::flattenSingleValue($rowNum); + $columnNum = Functions::flattenSingleValue($columnNum); + + if (!is_numeric($rowNum) || !is_numeric($columnNum) || ($rowNum < 0) || ($columnNum < 0)) { + return Functions::VALUE(); + } + + if (!is_array($matrix) || ($rowNum > count($matrix))) { + return Functions::REF(); + } + + $rowKeys = array_keys($matrix); + $columnKeys = @array_keys($matrix[$rowKeys[0]]); + + if ($columnNum > count($columnKeys)) { + return Functions::REF(); + } + + if ($columnNum == 0) { + return self::extractRowValue($matrix, $rowKeys, $rowNum); + } + + $columnNum = $columnKeys[--$columnNum]; + if ($rowNum == 0) { + return array_map( + function ($value) { + return [$value]; + }, + array_column($matrix, $columnNum) + ); + } + $rowNum = $rowKeys[--$rowNum]; + + return $matrix[$rowNum][$columnNum]; + } + + private static function extractRowValue(array $matrix, array $rowKeys, int $rowNum) + { + if ($rowNum == 0) { + return $matrix; + } + + $rowNum = $rowKeys[--$rowNum]; + $row = $matrix[$rowNum]; + if (is_array($row)) { + return [$rowNum => $row]; + } + + return $row; + } } diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php index 0cce806a..19d0d5ff 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/RowColumnInformation.php @@ -39,23 +39,23 @@ class RowColumnInformation return (int) Coordinate::columnIndexFromString($columnKey); } - } else { - [, $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); - $returnValue = []; - do { - $returnValue[] = (int) Coordinate::columnIndexFromString($startAddress); - } while ($startAddress++ != $endAddress); - - return $returnValue; - } - $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); - - return (int) Coordinate::columnIndexFromString($cellAddress); } + + [, $cellAddress] = Worksheet::extractSheetTitle((string) $cellAddress, true); + if (strpos($cellAddress, ':') !== false) { + [$startAddress, $endAddress] = explode(':', $cellAddress); + $startAddress = preg_replace('/[^a-z]/i', '', $startAddress); + $endAddress = preg_replace('/[^a-z]/i', '', $endAddress); + + return range( + (int) Coordinate::columnIndexFromString($startAddress), + (int) Coordinate::columnIndexFromString($endAddress) + ); + } + + $cellAddress = preg_replace('/[^a-z]/i', '', $cellAddress); + + return (int) Coordinate::columnIndexFromString($cellAddress); } /** @@ -73,7 +73,7 @@ class RowColumnInformation */ public static function COLUMNS($cellAddress = null) { - if ($cellAddress === null || $cellAddress === '') { + if ($cellAddress === null || (is_string($cellAddress) && trim($cellAddress) === '')) { return 1; } elseif (!is_array($cellAddress)) { return Functions::VALUE(); @@ -114,28 +114,29 @@ class RowColumnInformation } if (is_array($cellAddress)) { - foreach ($cellAddress as $columnKey => $rowValue) { - foreach ($rowValue as $rowKey => $cellValue) { + foreach ($cellAddress as $rowKey => $rowValue) { + foreach ($rowValue as $columnKey => $cellValue) { return (int) preg_replace('/\D/', '', $rowKey); } } - } else { - [, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - if (strpos($cellAddress, ':') !== false) { - [$startAddress, $endAddress] = explode(':', $cellAddress); - $startAddress = preg_replace('/\D/', '', $startAddress); - $endAddress = preg_replace('/\D/', '', $endAddress); - $returnValue = []; - do { - $returnValue[][] = (int) $startAddress; - } while ($startAddress++ != $endAddress); - - return $returnValue; - } - [$cellAddress] = explode(':', $cellAddress); - - return (int) preg_replace('/\D/', '', $cellAddress); } + + [, $cellAddress] = Worksheet::extractSheetTitle((string) $cellAddress, true); + if (strpos($cellAddress, ':') !== false) { + [$startAddress, $endAddress] = explode(':', $cellAddress); + $startAddress = preg_replace('/\D/', '', $startAddress); + $endAddress = preg_replace('/\D/', '', $endAddress); + + return array_map( + function ($value) { + return [$value]; + }, + range($startAddress, $endAddress) + ); + } + [$cellAddress] = explode(':', $cellAddress); + + return (int) preg_replace('/\D/', '', $cellAddress); } /** @@ -153,7 +154,7 @@ class RowColumnInformation */ public static function ROWS($cellAddress = null) { - if ($cellAddress === null || $cellAddress === '') { + if ($cellAddress === null || (is_string($cellAddress) && trim($cellAddress) === '')) { return 1; } elseif (!is_array($cellAddress)) { return Functions::VALUE(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php index 8ff66931..c84a504d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndexTest.php @@ -21,6 +21,7 @@ class IndexTest extends TestCase public function testINDEX($expectedResult, ...$args): void { $result = LookupRef::INDEX(...$args); +// var_dump($result); self::assertEquals($expectedResult, $result); } diff --git a/tests/data/Calculation/LookupRef/INDEX.php b/tests/data/Calculation/LookupRef/INDEX.php index 55206270..157794ab 100644 --- a/tests/data/Calculation/LookupRef/INDEX.php +++ b/tests/data/Calculation/LookupRef/INDEX.php @@ -54,7 +54,7 @@ return [ -1, ], [ - '#VALUE!', // Expected + '#REF!', // Expected // Input [ '20' => ['R' => 1, 'S' => 3], @@ -63,6 +63,16 @@ return [ 2, 10, ], + [ + '#REF!', // Expected + // Input + [ + '20' => ['R' => 1, 'S' => 3], + '21' => ['R' => 2, 'S' => 4], + ], + 10, + 2, + ], [ 4, // Expected // Input @@ -87,4 +97,64 @@ return [ '21' => ['R' => 2], ], ], + [ + 'Pears', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 2, + ], + [ + 'Bananas', + [ + ['Apples', 'Lemons'], + ['Bananas', 'Pears'], + ], + 2, + 1, + ], + [ + 3, + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 5, + 2, + ], + [ + [4 => [8, 3]], + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 5, + 0, + ], + [ + [ + [6], + [3], + [9], + [5], + [3], + ], + [ + [4, 6], + [5, 3], + [6, 9], + [7, 5], + [8, 3], + ], + 0, + 2, + ], ]; diff --git a/tests/data/Calculation/LookupRef/ROW.php b/tests/data/Calculation/LookupRef/ROW.php index 89566011..62545c76 100644 --- a/tests/data/Calculation/LookupRef/ROW.php +++ b/tests/data/Calculation/LookupRef/ROW.php @@ -17,6 +17,13 @@ return [ [[10], [11], [12]], 'C10:D12', ], + [ + 4, + [ + 4 => ['B' => 'B5', 'C' => 'C5', 'D' => 'D5'], + 5 => ['B' => 'B5', 'C' => 'C5', 'D' => 'D5'], + ], + ], [ [[10], [11], [12]], 'Sheet1!C10:C12', From baacc83995852934afd1417e97b55b19a54519cd Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 12 Mar 2021 18:23:15 +0100 Subject: [PATCH 33/89] Replace manual wildcard logic in MATCH() function with the new WildcardMatch methods (#1919) * Replace manual wildcard logic in MATCH() function with the new WildcardMatch methods * Additional unit tests * Refactor input validations * Refactor actual search logic into dedicated methods * Eliminate redundant code --- .../Calculation/Calculation.php | 2 +- src/PhpSpreadsheet/Calculation/LookupRef.php | 141 +------------ .../Calculation/LookupRef/ExcelMatch.php | 198 ++++++++++++++++++ tests/data/Calculation/LookupRef/MATCH.php | 84 +++++++- 4 files changed, 279 insertions(+), 146 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 3adcf243..80b6d47b 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1616,7 +1616,7 @@ class Calculation ], 'MATCH' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'MATCH'], + 'functionCall' => [LookupRef\ExcelMatch::class, 'MATCH'], 'argumentCount' => '2,3', ], 'MAX' => [ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index f1a147f1..d2cc4f94 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -10,7 +10,6 @@ use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\RowColumnInformation; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\VLookup; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class LookupRef @@ -380,145 +379,7 @@ class LookupRef */ public static function MATCH($lookupValue, $lookupArray, $matchType = 1) { - $lookupArray = Functions::flattenArray($lookupArray); - $lookupValue = Functions::flattenSingleValue($lookupValue); - $matchType = ($matchType === null) ? 1 : (int) Functions::flattenSingleValue($matchType); - - // MATCH is not case sensitive, so we convert lookup value to be lower cased in case it's string type. - if (is_string($lookupValue)) { - $lookupValue = StringHelper::strToLower($lookupValue); - } - - // Lookup_value type has to be number, text, or logical values - if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) { - return Functions::NA(); - } - - // Match_type is 0, 1 or -1 - if (($matchType !== 0) && ($matchType !== -1) && ($matchType !== 1)) { - return Functions::NA(); - } - - // Lookup_array should not be empty - $lookupArraySize = count($lookupArray); - if ($lookupArraySize <= 0) { - return Functions::NA(); - } - - if ($matchType == 1) { - // If match_type is 1 the list has to be processed from last to first - - $lookupArray = array_reverse($lookupArray); - $keySet = array_reverse(array_keys($lookupArray)); - } - - // Lookup_array should contain only number, text, or logical values, or empty (null) cells - foreach ($lookupArray as $i => $lookupArrayValue) { - // check the type of the value - if ( - (!is_numeric($lookupArrayValue)) && (!is_string($lookupArrayValue)) && - (!is_bool($lookupArrayValue)) && ($lookupArrayValue !== null) - ) { - return Functions::NA(); - } - // Convert strings to lowercase for case-insensitive testing - if (is_string($lookupArrayValue)) { - $lookupArray[$i] = StringHelper::strToLower($lookupArrayValue); - } - if (($lookupArrayValue === null) && (($matchType == 1) || ($matchType == -1))) { - unset($lookupArray[$i]); - } - } - - // ** - // find the match - // ** - - if ($matchType === 0 || $matchType === 1) { - foreach ($lookupArray as $i => $lookupArrayValue) { - $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) || (is_numeric($lookupValue) && is_numeric($lookupArrayValue))); - $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue; - $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue; - $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch; - - if ($matchType === 0) { - if ($typeMatch && is_string($lookupValue) && (bool) preg_match('/([\?\*])/', $lookupValue)) { - $splitString = $lookupValue; - $chars = array_map(function ($i) use ($splitString) { - return mb_substr($splitString, $i, 1); - }, range(0, mb_strlen($splitString) - 1)); - - $length = count($chars); - $pattern = '/^'; - for ($j = 0; $j < $length; ++$j) { - if ($chars[$j] === '~') { - if (isset($chars[$j + 1])) { - if ($chars[$j + 1] === '*') { - $pattern .= preg_quote($chars[$j + 1], '/'); - ++$j; - } elseif ($chars[$j + 1] === '?') { - $pattern .= preg_quote($chars[$j + 1], '/'); - ++$j; - } - } else { - $pattern .= preg_quote($chars[$j], '/'); - } - } elseif ($chars[$j] === '*') { - $pattern .= '.*'; - } elseif ($chars[$j] === '?') { - $pattern .= '.{1}'; - } else { - $pattern .= preg_quote($chars[$j], '/'); - } - } - - $pattern .= '$/'; - if ((bool) preg_match($pattern, $lookupArrayValue)) { - // exact match - return $i + 1; - } - } elseif ($exactMatch) { - // exact match - return $i + 1; - } - } elseif (($matchType === 1) && $typeMatch && ($lookupArrayValue <= $lookupValue)) { - $i = array_search($i, $keySet); - - // The current value is the (first) match - return $i + 1; - } - } - } else { - $maxValueKey = null; - - // The basic algorithm is: - // Iterate and keep the highest match until the next element is smaller than the searched value. - // Return immediately if perfect match is found - foreach ($lookupArray as $i => $lookupArrayValue) { - $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); - $exactTypeMatch = $typeMatch && $lookupArrayValue === $lookupValue; - $nonOnlyNumericExactMatch = !$typeMatch && $lookupArrayValue === $lookupValue; - $exactMatch = $exactTypeMatch || $nonOnlyNumericExactMatch; - - if ($exactMatch) { - // Another "special" case. If a perfect match is found, - // the algorithm gives up immediately - return $i + 1; - } elseif ($typeMatch & $lookupArrayValue >= $lookupValue) { - $maxValueKey = $i + 1; - } elseif ($typeMatch & $lookupArrayValue < $lookupValue) { - //Excel algorithm gives up immediately if the first element is smaller than the searched value - break; - } - } - - if ($maxValueKey !== null) { - return $maxValueKey; - } - } - - // Unsuccessful in finding a match, return #N/A error value - return Functions::NA(); + return LookupRef\ExcelMatch::MATCH($lookupValue, $lookupArray, $matchType); } /** diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php b/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php new file mode 100644 index 00000000..71358bf3 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/ExcelMatch.php @@ -0,0 +1,198 @@ +getMessage(); + } + + // MATCH() is not case sensitive, so we convert lookup value to be lower cased if it's a string type. + if (is_string($lookupValue)) { + $lookupValue = StringHelper::strToLower($lookupValue); + } + + $valueKey = null; + switch ($matchType) { + case self::MATCHTYPE_LARGEST_VALUE: + $valueKey = self::matchLargestValue($lookupArray, $lookupValue, $keySet); + + break; + case self::MATCHTYPE_FIRST_VALUE: + $valueKey = self::matchFirstValue($lookupArray, $lookupValue); + + break; + case self::MATCHTYPE_SMALLEST_VALUE: + default: + $valueKey = self::matchSmallestValue($lookupArray, $lookupValue); + } + + if ($valueKey !== null) { + return ++$valueKey; + } + + // Unsuccessful in finding a match, return #N/A error value + return Functions::NA(); + } + + private static function matchFirstValue($lookupArray, $lookupValue) + { + $wildcardLookup = ((bool) preg_match('/([\?\*])/', $lookupValue)); + $wildcard = WildcardMatch::wildcard($lookupValue); + + foreach ($lookupArray as $i => $lookupArrayValue) { + $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) || + (is_numeric($lookupValue) && is_numeric($lookupArrayValue))); + + if ( + $typeMatch && is_string($lookupValue) && + $wildcardLookup && WildcardMatch::compare($lookupArrayValue, $wildcard) + ) { + // wildcard match + return $i; + } elseif ($lookupArrayValue === $lookupValue) { + // exact match + return $i; + } + } + + return null; + } + + private static function matchLargestValue($lookupArray, $lookupValue, $keySet) + { + foreach ($lookupArray as $i => $lookupArrayValue) { + $typeMatch = ((gettype($lookupValue) === gettype($lookupArrayValue)) || + (is_numeric($lookupValue) && is_numeric($lookupArrayValue))); + + if ($typeMatch && ($lookupArrayValue <= $lookupValue)) { + return array_search($i, $keySet); + } + } + + return null; + } + + private static function matchSmallestValue($lookupArray, $lookupValue) + { + $valueKey = null; + + // The basic algorithm is: + // Iterate and keep the highest match until the next element is smaller than the searched value. + // Return immediately if perfect match is found + foreach ($lookupArray as $i => $lookupArrayValue) { + $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue); + + if ($lookupArrayValue === $lookupValue) { + // Another "special" case. If a perfect match is found, + // the algorithm gives up immediately + return $i; + } elseif ($typeMatch && $lookupArrayValue >= $lookupValue) { + $valueKey = $i; + } elseif ($typeMatch && $lookupArrayValue < $lookupValue) { + //Excel algorithm gives up immediately if the first element is smaller than the searched value + break; + } + } + + return $valueKey; + } + + private static function validateLookupValue($lookupValue): void + { + // 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()); + } + } + + private static function validateMatchType($matchType): void + { + // Match_type is 0, 1 or -1 + if ( + ($matchType !== self::MATCHTYPE_FIRST_VALUE) && + ($matchType !== self::MATCHTYPE_LARGEST_VALUE) && ($matchType !== self::MATCHTYPE_SMALLEST_VALUE) + ) { + throw new Exception(Functions::NA()); + } + } + + private static function validateLookupArray($lookupArray): void + { + // Lookup_array should not be empty + $lookupArraySize = count($lookupArray); + if ($lookupArraySize <= 0) { + throw new Exception(Functions::NA()); + } + } + + private static function prepareLookupArray($lookupArray, $matchType) + { + // Lookup_array should contain only number, text, or logical values, or empty (null) cells + 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()); + } + // Convert strings to lowercase for case-insensitive testing + if (is_string($value)) { + $lookupArray[$i] = StringHelper::strToLower($value); + } + if ( + ($value === null) && + (($matchType == self::MATCHTYPE_LARGEST_VALUE) || ($matchType == self::MATCHTYPE_SMALLEST_VALUE)) + ) { + unset($lookupArray[$i]); + } + } + + return $lookupArray; + } +} diff --git a/tests/data/Calculation/LookupRef/MATCH.php b/tests/data/Calculation/LookupRef/MATCH.php index 671056dd..03d9bfff 100644 --- a/tests/data/Calculation/LookupRef/MATCH.php +++ b/tests/data/Calculation/LookupRef/MATCH.php @@ -26,7 +26,6 @@ return [ [2, 0, 0, 3], 0, ], - // Third argument = 1 [ 1, // Expected @@ -52,7 +51,6 @@ return [ [2, 0, 0, 3], 1, ], - // Third argument = -1 [ 1, // Expected @@ -96,7 +94,12 @@ return [ [8, 8, 3, 2], -1, ], - + [ // Default matchtype + 4, // Expected + 4, // Input + [2, 0, 0, 3], + null, + ], // match on ranges with empty cells [ 3, // Expected @@ -110,7 +113,6 @@ return [ [1, null, 4, null, null], 1, ], - // 0s are causing errors, because things like 0 == 'x' is true. Thanks PHP! [ 3, @@ -233,7 +235,7 @@ return [ [ 2, // Expected 'a*~*c', - ['aAAAAA', 'a123456*c', 'az'], + ['aAAAAA', 'a123456*c', 'az', 'alembic'], 0, ], [ @@ -272,4 +274,76 @@ return [ [1, 22, 'aaa'], 0, ], + [ + '#N/A', // Expected + 'abc', + [1, 22, 'aaa'], + 0, + ], + [ + '#N/A', // Expected (Invalid lookup value) + new DateTime('2021-03-11'), + [1, 22, 'aaa'], + 1, + ], + [ + '#N/A', // Expected (Invalid match type) + 'abc', + [1, 22, 'aaa'], + 123, + ], + [ + '#N/A', // Expected (Empty lookup array) + 'abc', + [], + 1, + ], + [ + 8, + 'A*e', + ['Aardvark', 'Apple', 'Armadillo', 'Acre', 'Absolve', 'Amplitude', 'Adverse', 'Apartment'], + -1, + ], + [ + 2, + 'A*e', + ['Aardvark', 'Apple', 'Armadillo', 'Acre', 'Absolve', 'Amplitude', 'Adverse', 'Apartment'], + 0, + ], + [ + '#N/A', + 'A*e', + ['Aardvark', 'Apple', 'Armadillo', 'Acre', 'Absolve', 'Amplitude', 'Adverse', 'Apartment'], + 1, + ], + [ + 8, + 'A?s*e', + ['Aardvark', 'Apple', 'Armadillo', 'Acre', 'Absolve', 'Amplitude', 'Adverse', 'Apartment'], + -1, + ], + [ + 5, + 'A?s*e', + ['Aardvark', 'Apple', 'Armadillo', 'Acre', 'Absolve', 'Amplitude', 'Adverse', 'Apartment'], + 0, + ], + [ + '#N/A', + 'A*e', + ['Aardvark', 'Apple', 'Armadillo', 'Acre', 'Absolve', 'Amplitude', 'Adverse', 'Apartment'], + 1, + ], + [ + 8, + '*verse', + ['Obtuse', 'Amuse', 'Obverse', 'Inverse', 'Assurance', 'Amplitude', 'Adverse', 'Apartment'], + -1, + ], + [ + 3, + '*verse', + ['Obtuse', 'Amuse', 'Obverse', 'Inverse', 'Assurance', 'Amplitude', 'Adverse', 'Apartment'], + 0, + ], ]; From 0d1957ad2cc3fad228ef7709cfcbfb04b2927438 Mon Sep 17 00:00:00 2001 From: christof-b Date: Sat, 13 Mar 2021 12:03:05 +0100 Subject: [PATCH 34/89] Fix SpreadsheetML (xml) detection (#1917) * Fix SpreadsheetML (xml) detection (#1916) Replace the unrequired product signature by the required namespace definition for XML Spreadsheet. * Add summary to changelog (#1916) Co-authored-by: Christof Bachmann --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Reader/Xml.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b5e273f8..2565a768 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). - Fix for [Issue #1887](https://github.com/PHPOffice/PhpSpreadsheet/issues/1887) - Lose Track of Selected Cells After Save - Fixed issue with Xlsx@listWorksheetInfo not returning any data - Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions. [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640) +- Fix for [Issue #1916](https://github.com/PHPOffice/PhpSpreadsheet/issues/1916) - Invalid signature check for XML files ## 1.17.1 - 2021-03-01 diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index b1df0ef5..a827d17a 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -116,7 +116,7 @@ class Xml extends BaseReader $signature = [ '', + 'xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet', ]; // Open file From 0ce8509a8cc5e9f0bc3166b0abfbbff2c4425b2b Mon Sep 17 00:00:00 2001 From: oleibman Date: Sat, 13 Mar 2021 03:06:30 -0800 Subject: [PATCH 35/89] Continue MathTrig Breakup - Trig Functions (#1905) * Continue MathTrig Breakup - Trig Functions Continuing the process of breaking MathTrip.php up into smaller classes. This round takes care of the trig and hyperbolic functions, plus a few others. - COS, COSH, ACOS, ACOSH - COT, COTH, ACOT, ACOTH - CSC, CSCH - SEC, SECH - SIN, SINH, ASIN, ASINH - TAN, TANH, ATAN, ATANH, ATAN2 - EVEN - ODD - SIGN There are no bug fixes in this PR, except that boolean arguments are now accepted for all these functions, as they are for Excel. Taking a cue from what has been done in Engineering, the parameter validation now happens in a routine which issues Exceptions for invalid values; this simplifies the code in the functions themselves. Consistent with earlier changes of this nature, the versions in the MathTrig class remain, with a doc block indicating deprecation, and a stub call to the new routines. I think several more iterations will be needed to break up MathTrig completely. --- .../Calculation/Calculation.php | 46 +-- src/PhpSpreadsheet/Calculation/MathTrig.php | 345 +++++------------- .../Calculation/MathTrig/Acos.php | 28 ++ .../Calculation/MathTrig/Acosh.php | 28 ++ .../Calculation/MathTrig/Acot.php | 28 ++ .../Calculation/MathTrig/Acoth.php | 30 ++ .../Calculation/MathTrig/Asin.php | 28 ++ .../Calculation/MathTrig/Asinh.php | 28 ++ .../Calculation/MathTrig/Atan.php | 28 ++ .../Calculation/MathTrig/Atan2.php | 46 +++ .../Calculation/MathTrig/Atanh.php | 28 ++ .../Calculation/MathTrig/Ceiling.php | 16 +- .../Calculation/MathTrig/CeilingMath.php | 31 +- .../Calculation/MathTrig/CeilingPrecise.php | 25 +- .../Calculation/MathTrig/Cos.php | 28 ++ .../Calculation/MathTrig/Cosh.php | 28 ++ .../Calculation/MathTrig/Cot.php | 28 ++ .../Calculation/MathTrig/Coth.php | 28 ++ .../Calculation/MathTrig/Csc.php | 28 ++ .../Calculation/MathTrig/Csch.php | 28 ++ .../Calculation/MathTrig/Even.php | 35 ++ .../Calculation/MathTrig/Floor.php | 18 +- .../Calculation/MathTrig/FloorMath.php | 22 +- .../Calculation/MathTrig/FloorPrecise.php | 14 +- .../Calculation/MathTrig/Helpers.php | 87 +++++ .../Calculation/MathTrig/IntClass.php | 12 +- .../Calculation/MathTrig/Mround.php | 34 +- .../Calculation/MathTrig/Odd.php | 38 ++ .../Calculation/MathTrig/Roman.php | 32 +- .../Calculation/MathTrig/Round.php | 14 +- .../Calculation/MathTrig/RoundDown.php | 31 +- .../Calculation/MathTrig/RoundUp.php | 31 +- .../Calculation/MathTrig/Sec.php | 28 ++ .../Calculation/MathTrig/Sech.php | 28 ++ .../Calculation/MathTrig/Sign.php | 29 ++ .../Calculation/MathTrig/Sin.php | 28 ++ .../Calculation/MathTrig/Sinh.php | 28 ++ .../Calculation/MathTrig/Tan.php | 28 ++ .../Calculation/MathTrig/Tanh.php | 28 ++ .../Calculation/MathTrig/Trunc.php | 15 +- .../Functions/MathTrig/AcosTest.php | 11 +- .../Functions/MathTrig/AcoshTest.php | 11 +- .../Functions/MathTrig/AcotTest.php | 23 +- .../Functions/MathTrig/AcothTest.php | 23 +- .../Functions/MathTrig/AsinTest.php | 11 +- .../Functions/MathTrig/AsinhTest.php | 11 +- .../Functions/MathTrig/Atan2Test.php | 25 +- .../Functions/MathTrig/AtanTest.php | 11 +- .../Functions/MathTrig/AtanhTest.php | 11 +- .../Functions/MathTrig/CosTest.php | 11 +- .../Functions/MathTrig/CoshTest.php | 11 +- .../Functions/MathTrig/CotTest.php | 23 +- .../Functions/MathTrig/CothTest.php | 23 +- .../Functions/MathTrig/CscTest.php | 23 +- .../Functions/MathTrig/CschTest.php | 23 +- .../Functions/MathTrig/EvenTest.php | 19 +- .../Functions/MathTrig/MovedFunctionsTest.php | 32 ++ .../Functions/MathTrig/OddTest.php | 19 +- .../Functions/MathTrig/SecTest.php | 23 +- .../Functions/MathTrig/SechTest.php | 23 +- .../Functions/MathTrig/SignTest.php | 22 +- .../Functions/MathTrig/SinTest.php | 11 +- .../Functions/MathTrig/SinhTest.php | 11 +- .../Functions/MathTrig/TanTest.php | 11 +- .../Functions/MathTrig/TanhTest.php | 11 +- tests/data/Calculation/MathTrig/ACOS.php | 14 +- tests/data/Calculation/MathTrig/ACOSH.php | 14 +- tests/data/Calculation/MathTrig/ACOT.php | 75 +--- tests/data/Calculation/MathTrig/ACOTH.php | 75 +--- tests/data/Calculation/MathTrig/ASIN.php | 14 +- tests/data/Calculation/MathTrig/ASINH.php | 16 +- tests/data/Calculation/MathTrig/ATAN.php | 16 +- tests/data/Calculation/MathTrig/ATAN2.php | 84 +---- tests/data/Calculation/MathTrig/ATANH.php | 16 +- tests/data/Calculation/MathTrig/COS.php | 18 +- tests/data/Calculation/MathTrig/COSH.php | 12 +- tests/data/Calculation/MathTrig/COT.php | 65 +--- tests/data/Calculation/MathTrig/COTH.php | 76 +--- tests/data/Calculation/MathTrig/CSC.php | 65 +--- tests/data/Calculation/MathTrig/CSCH.php | 75 +--- tests/data/Calculation/MathTrig/EVEN.php | 88 +---- tests/data/Calculation/MathTrig/ODD.php | 68 +--- tests/data/Calculation/MathTrig/SEC.php | 85 ++--- tests/data/Calculation/MathTrig/SECH.php | 75 +--- tests/data/Calculation/MathTrig/SIGN.php | 71 +--- tests/data/Calculation/MathTrig/SIN.php | 16 +- tests/data/Calculation/MathTrig/SINH.php | 14 +- tests/data/Calculation/MathTrig/TAN.php | 20 +- tests/data/Calculation/MathTrig/TANH.php | 14 +- 89 files changed, 1619 insertions(+), 1383 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Acos.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Acosh.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Acot.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Acoth.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Asin.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Asinh.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Atan.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Atan2.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Atanh.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Cos.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Cosh.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Cot.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Coth.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Csc.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Csch.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Even.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Odd.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Sec.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Sech.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Sign.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Sin.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Sinh.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Tan.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Tanh.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 80b6d47b..1db4722b 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -243,22 +243,22 @@ class Calculation ], 'ACOS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinACOS'], + 'functionCall' => [MathTrig\Acos::class, 'funcAcos'], 'argumentCount' => '1', ], 'ACOSH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinACOSH'], + 'functionCall' => [MathTrig\Acosh::class, 'funcAcosh'], 'argumentCount' => '1', ], 'ACOT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'ACOT'], + 'functionCall' => [MathTrig\Acot::class, 'funcAcot'], 'argumentCount' => '1', ], 'ACOTH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'ACOTH'], + 'functionCall' => [MathTrig\Acoth::class, 'funcAcoth'], 'argumentCount' => '1', ], 'ADDRESS' => [ @@ -303,27 +303,27 @@ class Calculation ], 'ASIN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinASIN'], + 'functionCall' => [MathTrig\Asin::class, 'funcAsin'], 'argumentCount' => '1', ], 'ASINH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinASINH'], + 'functionCall' => [MathTrig\Asinh::class, 'funcAsinh'], 'argumentCount' => '1', ], 'ATAN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinATAN'], + 'functionCall' => [MathTrig\Atan::class, 'funcAtan'], 'argumentCount' => '1', ], 'ATAN2' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'ATAN2'], + 'functionCall' => [MathTrig\Atan2::class, 'funcAtan2'], 'argumentCount' => '2', ], 'ATANH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinATANH'], + 'functionCall' => [MathTrig\Atanh::class, 'funcAtanh'], 'argumentCount' => '1', ], 'AVEDEV' => [ @@ -605,22 +605,22 @@ class Calculation ], 'COS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinCOS'], + 'functionCall' => [MathTrig\Cos::class, 'funcCos'], 'argumentCount' => '1', ], 'COSH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinCOSH'], + 'functionCall' => [MathTrig\Cosh::class, 'funcCosh'], 'argumentCount' => '1', ], 'COT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'COT'], + 'functionCall' => [MathTrig\Cot::class, 'funcCot'], 'argumentCount' => '1', ], 'COTH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'COTH'], + 'functionCall' => [MathTrig\Coth::class, 'funcCoth'], 'argumentCount' => '1', ], 'COUNT' => [ @@ -700,12 +700,12 @@ class Calculation ], 'CSC' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'CSC'], + 'functionCall' => [MathTrig\Csc::class, 'funcCsc'], 'argumentCount' => '1', ], 'CSCH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'CSCH'], + 'functionCall' => [MathTrig\Csch::class, 'funcCsch'], 'argumentCount' => '1', ], 'CUBEKPIMEMBER' => [ @@ -965,7 +965,7 @@ class Calculation ], 'EVEN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'EVEN'], + 'functionCall' => [MathTrig\Even::class, 'funcEven'], 'argumentCount' => '1', ], 'EXACT' => [ @@ -1856,7 +1856,7 @@ class Calculation ], 'ODD' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'ODD'], + 'functionCall' => [MathTrig\Odd::class, 'funcOdd'], 'argumentCount' => '1', ], 'ODDFPRICE' => [ @@ -2165,12 +2165,12 @@ class Calculation ], 'SEC' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SEC'], + 'functionCall' => [MathTrig\Sec::class, 'funcSec'], 'argumentCount' => '1', ], 'SECH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SECH'], + 'functionCall' => [MathTrig\Sech::class, 'funcSech'], 'argumentCount' => '1', ], 'SECOND' => [ @@ -2200,7 +2200,7 @@ class Calculation ], 'SIGN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SIGN'], + 'functionCall' => [MathTrig\Sign::class, 'funcSign'], 'argumentCount' => '1', ], 'SIN' => [ @@ -2210,7 +2210,7 @@ class Calculation ], 'SINH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinSINH'], + 'functionCall' => [MathTrig\Sinh::class, 'funcSinh'], 'argumentCount' => '1', ], 'SKEW' => [ @@ -2366,12 +2366,12 @@ class Calculation ], 'TAN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinTAN'], + 'functionCall' => [MathTrig\Tan::class, 'funcTan'], 'argumentCount' => '1', ], 'TANH' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinTANH'], + 'functionCall' => [MathTrig\Tanh::class, 'funcTanh'], 'argumentCount' => '1', ], 'TBILLEQ' => [ diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index e72e24cd..f3d8351d 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -133,6 +133,8 @@ class MathTrig * Note that the Excel ATAN2() function accepts its arguments in the reverse order to the standard * PHP atan2() function, so we need to reverse them here before calling the PHP atan() function. * + * @Deprecated 2.0.0 Use the funcAtan2 method in the MathTrig\Atan2 class instead + * * Excel Function: * ATAN2(xCoordinate,yCoordinate) * @@ -143,27 +145,7 @@ class MathTrig */ public static function ATAN2($xCoordinate = null, $yCoordinate = null) { - $xCoordinate = Functions::flattenSingleValue($xCoordinate); - $yCoordinate = Functions::flattenSingleValue($yCoordinate); - - $xCoordinate = $xCoordinate ?? 0.0; - $yCoordinate = $yCoordinate ?? 0.0; - - if ( - ((is_numeric($xCoordinate)) || (is_bool($xCoordinate))) && - ((is_numeric($yCoordinate))) || (is_bool($yCoordinate)) - ) { - $xCoordinate = (float) $xCoordinate; - $yCoordinate = (float) $yCoordinate; - - if (($xCoordinate == 0) && ($yCoordinate == 0)) { - return Functions::DIV0(); - } - - return atan2($yCoordinate, $xCoordinate); - } - - return Functions::VALUE(); + return MathTrig\Atan2::funcAtan2($xCoordinate, $yCoordinate); } /** @@ -226,8 +208,6 @@ class MathTrig * @param float $significance the multiple to which you want to round * * @return float|string Rounded Number, or a string containing an error - * - * @codeCoverageIgnore */ public static function CEILING($number, $significance = null) { @@ -269,6 +249,8 @@ class MathTrig /** * EVEN. * + * @Deprecated 2.0.0 Use the funcEven method in the MathTrig\Even class instead + * * Returns number rounded up to the nearest even integer. * You can use this function for processing items that come in twos. For example, * a packing crate accepts rows of one or two items. The crate is full when @@ -284,26 +266,17 @@ class MathTrig */ public static function EVEN($number) { - $number = Functions::flattenSingleValue($number); - - if ($number === null) { - return 0; - } elseif (is_bool($number)) { - $number = (int) $number; - } - - if (is_numeric($number)) { - return self::getEven((float) $number); - } - - return Functions::VALUE(); + return MathTrig\Even::funcEven($number); } + /** + * Helper function for Even. + * + * @Deprecated 2.0.0 Use the getEven method in the MathTrig\Helpers class instead + */ public static function getEven(float $number): int { - $significance = 2 * self::returnSign($number); - - return (int) MathTrig\Ceiling::funcCeiling($number, $significance); + return (int) MathTrig\Helpers::getEven($number); } /** @@ -395,8 +368,6 @@ class MathTrig * @param float $significance Significance * * @return float|string Rounded Number, or a string containing an error - * - * @codeCoverageIgnore */ public static function FLOOR($number, $significance = null) { @@ -420,8 +391,6 @@ class MathTrig * @param int $mode direction to round negative numbers * * @return float|string Rounded Number, or a string containing an error - * - * @codeCoverageIgnore */ public static function FLOORMATH($number, $significance = null, $mode = 0) { @@ -444,8 +413,6 @@ class MathTrig * @param float $significance Significance * * @return float|string Rounded Number, or a string containing an error - * - * @codeCoverageIgnore */ public static function FLOORPRECISE($number, $significance = 1) { @@ -472,8 +439,6 @@ class MathTrig * @param float $number Number to cast to an integer * * @return int|string Integer value, or a string containing an error - * - * @codeCoverageIgnore */ public static function INT($number) { @@ -797,8 +762,6 @@ class MathTrig * @param int $multiple Multiple to which you want to round $number * * @return float|string Rounded Number, or a string containing an error - * - * @codeCoverageIgnore */ public static function MROUND($number, $multiple) { @@ -847,36 +810,15 @@ class MathTrig * * Returns number rounded up to the nearest odd integer. * + * @Deprecated 2.0.0 Use the funcOdd method in the MathTrig\Odd class instead + * * @param float $number Number to round * * @return int|string Rounded Number, or a string containing an error */ public static function ODD($number) { - $number = Functions::flattenSingleValue($number); - - if ($number === null) { - return 1; - } elseif (is_bool($number)) { - return 1; - } elseif (is_numeric($number)) { - $significance = self::returnSign($number); - if ($significance == 0) { - return 1; - } - - $result = MathTrig\Ceiling::funcCeiling($number, $significance); - if (is_string($result)) { - return $result; - } - if ($result == self::getEven((float) $result)) { - $result += $significance; - } - - return (int) $result; - } - - return Functions::VALUE(); + return MathTrig\Odd::funcOdd($number); } /** @@ -1015,8 +957,6 @@ class MathTrig * @param mixed $style Number indicating one of five possible forms * * @return string Roman numeral, or a string containing an error - * - * @codeCoverageIgnore */ public static function ROMAN($aValue, $style = 0) { @@ -1036,8 +976,6 @@ class MathTrig * @param int $digits Number of digits to which you want to round $number * * @return float|string Rounded Number, or a string containing an error - * - * @codeCoverageIgnore */ public static function ROUNDUP($number, $digits) { @@ -1057,8 +995,6 @@ class MathTrig * @param int $digits Number of digits to which you want to round $number * * @return float|string Rounded Number, or a string containing an error - * - * @codeCoverageIgnore */ public static function ROUNDDOWN($number, $digits) { @@ -1109,27 +1045,25 @@ class MathTrig * 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. * + * @Deprecated 2.0.0 Use the funcSign method in the MathTrig\Sign class instead + * * @param float $number Number to round * * @return int|string sign value, or a string containing an error */ public static function SIGN($number) { - $number = Functions::flattenSingleValue($number); - - if (is_bool($number)) { - return (int) $number; - } - if (is_numeric($number)) { - return self::returnSign($number); - } - - return Functions::VALUE(); + return MathTrig\Sign::funcSign($number); } + /** + * returnSign = returns 0/-1/+1. + * + * @Deprecated 2.0.0 Use the returnSign method in the MathTrig\Helpers class instead + */ public static function returnSign(float $number): int { - return $number ? (($number > 0) ? 1 : -1) : 0; + return MathTrig\Helpers::returnSign($number); } /** @@ -1486,8 +1420,6 @@ class MathTrig * @param int $digits * * @return float|string Truncated value, or a string containing an error - * - * @codeCoverageIgnore */ public static function TRUNC($value = 0, $digits = 0) { @@ -1499,21 +1431,15 @@ class MathTrig * * Returns the secant of an angle. * + * @Deprecated 2.0.0 Use the funcSec method in the MathTrig\Sec class instead + * * @param float $angle Number * * @return float|string The secant of the angle */ public static function SEC($angle) { - $angle = Functions::flattenSingleValue($angle); - - if (!is_numeric($angle)) { - return Functions::VALUE(); - } - - $result = cos($angle); - - return self::verySmallDivisor($result) ? Functions::DIV0() : (1 / $result); + return MathTrig\Sec::funcSec($angle); } /** @@ -1521,21 +1447,15 @@ class MathTrig * * Returns the hyperbolic secant of an angle. * + * @Deprecated 2.0.0 Use the funcSech method in the MathTrig\Sech class instead + * * @param float $angle Number * * @return float|string The hyperbolic secant of the angle */ public static function SECH($angle) { - $angle = Functions::flattenSingleValue($angle); - - if (!is_numeric($angle)) { - return Functions::VALUE(); - } - - $result = cosh($angle); - - return ($result == 0.0) ? Functions::DIV0() : 1 / $result; + return MathTrig\Sech::funcSech($angle); } /** @@ -1543,21 +1463,15 @@ class MathTrig * * Returns the cosecant of an angle. * + * @Deprecated 2.0.0 Use the funcCsc method in the MathTrig\Csc class instead + * * @param float $angle Number * * @return float|string The cosecant of the angle */ public static function CSC($angle) { - $angle = Functions::flattenSingleValue($angle); - - if (!is_numeric($angle)) { - return Functions::VALUE(); - } - - $result = sin($angle); - - return self::verySmallDivisor($result) ? Functions::DIV0() : (1 / $result); + return MathTrig\Csc::funcCsc($angle); } /** @@ -1565,21 +1479,15 @@ class MathTrig * * Returns the hyperbolic cosecant of an angle. * + * @Deprecated 2.0.0 Use the funcCsch method in the MathTrig\Csch class instead + * * @param float $angle Number * * @return float|string The hyperbolic cosecant of the angle */ public static function CSCH($angle) { - $angle = Functions::flattenSingleValue($angle); - - if (!is_numeric($angle)) { - return Functions::VALUE(); - } - - $result = sinh($angle); - - return ($result == 0.0) ? Functions::DIV0() : 1 / $result; + return MathTrig\Csch::funcCsch($angle); } /** @@ -1587,21 +1495,15 @@ class MathTrig * * Returns the cotangent of an angle. * + * @Deprecated 2.0.0 Use the funcCot method in the MathTrig\Cot class instead + * * @param float $angle Number * * @return float|string The cotangent of the angle */ public static function COT($angle) { - $angle = Functions::flattenSingleValue($angle); - - if (!is_numeric($angle)) { - return Functions::VALUE(); - } - - $result = sin($angle); - - return self::verySmallDivisor($result) ? Functions::DIV0() : (cos($angle) / $result); + return MathTrig\Cot::funcCot($angle); } /** @@ -1609,21 +1511,15 @@ class MathTrig * * Returns the hyperbolic cotangent of an angle. * + * @Deprecated 2.0.0 Use the funcCoth method in the MathTrig\Coth class instead + * * @param float $angle Number * * @return float|string The hyperbolic cotangent of the angle */ public static function COTH($angle) { - $angle = Functions::flattenSingleValue($angle); - - if (!is_numeric($angle)) { - return Functions::VALUE(); - } - - $result = tanh($angle); - - return ($result == 0.0) ? Functions::DIV0() : 1 / $result; + return MathTrig\Coth::funcCoth($angle); } /** @@ -1631,31 +1527,29 @@ class MathTrig * * Returns the arccotangent of a number. * + * @Deprecated 2.0.0 Use the funcAcot method in the MathTrig\Acot class instead + * * @param float $number Number * * @return float|string The arccotangent of the number */ public static function ACOT($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return (M_PI / 2) - atan($number); + return MathTrig\Acot::funcAcot($number); } /** * Return NAN or value depending on argument. * + * @Deprecated 2.0.0 Use the numberOrNan method in the MathTrig\Helpers class instead + * * @param float $result Number * * @return float|string */ public static function numberOrNan($result) { - return is_nan($result) ? Functions::NAN() : $result; + return MathTrig\Helpers::numberOrNan($result); } /** @@ -1663,21 +1557,15 @@ class MathTrig * * Returns the hyperbolic arccotangent of a number. * + * @Deprecated 2.0.0 Use the funcAcoth method in the MathTrig\Acoth class instead + * * @param float $number Number * * @return float|string The hyperbolic arccotangent of the number */ public static function ACOTH($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - $result = log(($number + 1) / ($number - 1)) / 2; - - return self::numberOrNan($result); + return MathTrig\Acoth::funcAcoth($number); } /** @@ -1693,8 +1581,6 @@ class MathTrig * @param mixed $precision Should be int * * @return float|string Rounded number - * - * @codeCoverageIgnore */ public static function builtinROUND($number, $precision) { @@ -1724,6 +1610,8 @@ class MathTrig /** * ACOS. * + * @Deprecated 2.0.0 Use the funcAcos method in the MathTrig\Acos class instead + * * Returns the result of builtin function acos after validating args. * * @param mixed $number Should be numeric @@ -1732,13 +1620,7 @@ class MathTrig */ public static function builtinACOS($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return self::numberOrNan(acos($number)); + return MathTrig\Acos::funcAcos($number); } /** @@ -1746,19 +1628,15 @@ class MathTrig * * Returns the result of builtin function acosh after validating args. * + * @Deprecated 2.0.0 Use the funcAcosh method in the MathTrig\Acosh class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinACOSH($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return self::numberOrNan(acosh($number)); + return MathTrig\Acosh::funcAcosh($number); } /** @@ -1766,19 +1644,15 @@ class MathTrig * * Returns the result of builtin function asin after validating args. * + * @Deprecated 2.0.0 Use the funcAsin method in the MathTrig\Asin class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinASIN($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return self::numberOrNan(asin($number)); + return MathTrig\Asin::funcAsin($number); } /** @@ -1786,39 +1660,31 @@ class MathTrig * * Returns the result of builtin function asinh after validating args. * + * @Deprecated 2.0.0 Use the funcAsinh method in the MathTrig\Asinh class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinASINH($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return asinh($number); + return MathTrig\Asinh::funcAsinh($number); } /** - * ASIN. + * ATAN. * * Returns the result of builtin function atan after validating args. * + * @Deprecated 2.0.0 Use the funcAtan method in the MathTrig\Atan class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinATAN($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return self::numberOrNan(atan($number)); + return MathTrig\Atan::funcAtan($number); } /** @@ -1826,19 +1692,15 @@ class MathTrig * * Returns the result of builtin function atanh after validating args. * + * @Deprecated 2.0.0 Use the funcAtanh method in the MathTrig\Atanh class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinATANH($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return atanh($number); + return MathTrig\Atanh::funcAtanh($number); } /** @@ -1846,19 +1708,15 @@ class MathTrig * * Returns the result of builtin function cos after validating args. * + * @Deprecated 2.0.0 Use the funcCos method in the MathTrig\Cos class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinCOS($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return cos($number); + return MathTrig\Cos::funcCos($number); } /** @@ -1866,19 +1724,15 @@ class MathTrig * * Returns the result of builtin function cos after validating args. * + * @Deprecated 2.0.0 Use the funcCosh method in the MathTrig\Cosh class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinCOSH($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return cosh($number); + return MathTrig\Cosh::funcCosh($number); } /** @@ -1984,6 +1838,8 @@ class MathTrig /** * SIN. * + * @Deprecated 2.0.0 Use the funcSin method in the MathTrig\Sin class instead + * * Returns the result of builtin function sin after validating args. * * @param mixed $number Should be numeric @@ -1992,18 +1848,14 @@ class MathTrig */ public static function builtinSIN($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return sin($number); + return MathTrig\Sin::funcSin($number); } /** * SINH. * + * @Deprecated 2.0.0 Use the funcSinh method in the MathTrig\Sinh class instead + * * Returns the result of builtin function sinh after validating args. * * @param mixed $number Should be numeric @@ -2012,13 +1864,7 @@ class MathTrig */ public static function builtinSINH($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return sinh($number); + return MathTrig\Sinh::funcSinh($number); } /** @@ -2046,19 +1892,15 @@ class MathTrig * * Returns the result of builtin function tan after validating args. * + * @Deprecated 2.0.0 Use the funcTan method in the MathTrig\Tan class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinTAN($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return self::verySmallDivisor(cos($number)) ? Functions::DIV0() : tan($number); + return MathTrig\Tan::funcTan($number); } /** @@ -2066,29 +1908,22 @@ class MathTrig * * Returns the result of builtin function sinh after validating args. * + * @Deprecated 2.0.0 Use the funcTanh method in the MathTrig\Tanh class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinTANH($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return tanh($number); - } - - private static function verySmallDivisor(float $number): bool - { - return abs($number) < 1.0E-12; + return MathTrig\Tanh::funcTanh($number); } /** * Many functions accept null/false/true argument treated as 0/0/1. * + * @Deprecated 2.0.0 Use the validateNumericNullBool method in the MathTrig\Helpers class instead + * * @param mixed $number */ public static function nullFalseTrueToNumber(&$number): void diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Acos.php b/src/PhpSpreadsheet/Calculation/MathTrig/Acos.php new file mode 100644 index 00000000..20d645f2 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Acos.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::numberOrNan(acos($number)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Acosh.php b/src/PhpSpreadsheet/Calculation/MathTrig/Acosh.php new file mode 100644 index 00000000..f77d3a09 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Acosh.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::numberOrNan(acosh($number)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Acot.php b/src/PhpSpreadsheet/Calculation/MathTrig/Acot.php new file mode 100644 index 00000000..1024f9f6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Acot.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return (M_PI / 2) - atan($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Acoth.php b/src/PhpSpreadsheet/Calculation/MathTrig/Acoth.php new file mode 100644 index 00000000..42bdc181 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Acoth.php @@ -0,0 +1,30 @@ +getMessage(); + } + + $result = ($number === 1) ? NAN : (log(($number + 1) / ($number - 1)) / 2); + + return Helpers::numberOrNan($result); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Asin.php b/src/PhpSpreadsheet/Calculation/MathTrig/Asin.php new file mode 100644 index 00000000..e30ab04c --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Asin.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::numberOrNan(asin($number)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Asinh.php b/src/PhpSpreadsheet/Calculation/MathTrig/Asinh.php new file mode 100644 index 00000000..35a3ae26 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Asinh.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::numberOrNan(asinh($number)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Atan.php b/src/PhpSpreadsheet/Calculation/MathTrig/Atan.php new file mode 100644 index 00000000..3e57f048 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Atan.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::numberOrNan(atan($number)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Atan2.php b/src/PhpSpreadsheet/Calculation/MathTrig/Atan2.php new file mode 100644 index 00000000..2ea975a8 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Atan2.php @@ -0,0 +1,46 @@ +getMessage(); + } + + if (($xCoordinate == 0) && ($yCoordinate == 0)) { + return Functions::DIV0(); + } + + return atan2($yCoordinate, $xCoordinate); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Atanh.php b/src/PhpSpreadsheet/Calculation/MathTrig/Atanh.php new file mode 100644 index 00000000..a9723f16 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Atanh.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::numberOrNan(atanh($number)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php b/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php index 6fdd6165..1085158a 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Ceiling.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class Ceiling { @@ -26,19 +25,18 @@ class Ceiling */ public static function funcCeiling($number, $significance = null) { - MathTrig::nullFalseTrueToNumber($number); - $significance = Functions::flattenSingleValue($significance); - if ($significance === null) { self::floorCheck1Arg(); - $significance = ((float) $number < 0) ? -1 : 1; } - if ((is_numeric($number)) && (is_numeric($significance))) { - return self::argumentsOk((float) $number, (float) $significance); + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + return self::argumentsOk((float) $number, (float) $significance); } /** @@ -51,7 +49,7 @@ class Ceiling if (empty($number * $significance)) { return 0.0; } - if (MathTrig::returnSign($number) == MathTrig::returnSign($significance)) { + if (Helpers::returnSign($number) == Helpers::returnSign($significance)) { return ceil($number / $significance) * $significance; } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/CeilingMath.php b/src/PhpSpreadsheet/Calculation/MathTrig/CeilingMath.php index f94d1fe0..e41e9d09 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/CeilingMath.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/CeilingMath.php @@ -2,8 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; class CeilingMath { @@ -23,26 +22,22 @@ class CeilingMath */ public static function funcCeilingMath($number, $significance = null, $mode = 0) { - MathTrig::nullFalseTrueToNumber($number); - $significance = Functions::flattenSingleValue($significance); - $mode = Functions::flattenSingleValue($mode); - - if ($significance === null) { - $significance = ((float) $number < 0) ? -1 : 1; + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + $mode = Helpers::validateNumericNullSubstitution($mode, null); + } catch (Exception $e) { + return $e->getMessage(); } - if (is_numeric($number) && is_numeric($significance) && is_numeric($mode)) { - if (empty($significance * $number)) { - return 0.0; - } - if (self::ceilingMathTest((float) $significance, (float) $number, (int) $mode)) { - return floor($number / $significance) * $significance; - } - - return ceil($number / $significance) * $significance; + if (empty($significance * $number)) { + return 0.0; + } + if (self::ceilingMathTest((float) $significance, (float) $number, (int) $mode)) { + return floor($number / $significance) * $significance; } - return Functions::VALUE(); + return ceil($number / $significance) * $significance; } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/CeilingPrecise.php b/src/PhpSpreadsheet/Calculation/MathTrig/CeilingPrecise.php index d9c61c41..1bc4504b 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/CeilingPrecise.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/CeilingPrecise.php @@ -2,8 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; class CeilingPrecise { @@ -22,18 +21,18 @@ class CeilingPrecise */ public static function funcCeilingPrecise($number, $significance = 1) { - MathTrig::nullFalseTrueToNumber($number); - $significance = Functions::flattenSingleValue($significance); - - if ((is_numeric($number)) && (is_numeric($significance))) { - if ($significance == 0.0) { - return 0.0; - } - $result = $number / abs($significance); - - return ceil($result) * $significance * (($significance < 0) ? -1 : 1); + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, null); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if (!$significance) { + return 0.0; + } + $result = $number / abs($significance); + + return ceil($result) * $significance * (($significance < 0) ? -1 : 1); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Cos.php b/src/PhpSpreadsheet/Calculation/MathTrig/Cos.php new file mode 100644 index 00000000..2dfed782 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Cos.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return cos($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Cosh.php b/src/PhpSpreadsheet/Calculation/MathTrig/Cosh.php new file mode 100644 index 00000000..3e806cd6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Cosh.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return cosh($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Cot.php b/src/PhpSpreadsheet/Calculation/MathTrig/Cot.php new file mode 100644 index 00000000..1cf5c6b4 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Cot.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::verySmallDenominator(cos($angle), sin($angle)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Coth.php b/src/PhpSpreadsheet/Calculation/MathTrig/Coth.php new file mode 100644 index 00000000..c80ec93e --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Coth.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::verySmallDenominator(1.0, tanh($angle)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Csc.php b/src/PhpSpreadsheet/Calculation/MathTrig/Csc.php new file mode 100644 index 00000000..325637b8 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Csc.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::verySmallDenominator(1.0, sin($angle)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Csch.php b/src/PhpSpreadsheet/Calculation/MathTrig/Csch.php new file mode 100644 index 00000000..8a045203 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Csch.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::verySmallDenominator(1.0, sinh($angle)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Even.php b/src/PhpSpreadsheet/Calculation/MathTrig/Even.php new file mode 100644 index 00000000..ac79a211 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Even.php @@ -0,0 +1,35 @@ +getMessage(); + } + + return Helpers::getEven($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php b/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php index 0c65ee7c..f178b324 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Floor.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class Floor { @@ -31,19 +30,18 @@ class Floor */ public static function funcFloor($number, $significance = null) { - MathTrig::nullFalseTrueToNumber($number); - $significance = Functions::flattenSingleValue($significance); - if ($significance === null) { self::floorCheck1Arg(); - $significance = MathTrig::returnSign((float) $number); } - if ((is_numeric($number)) && (is_numeric($significance))) { - return self::argumentsOk((float) $number, (float) $significance); + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + return self::argumentsOk((float) $number, (float) $significance); } /** @@ -59,10 +57,10 @@ class Floor if ($number == 0.0) { return 0.0; } - if (MathTrig::returnSign($significance) == 1) { + if (Helpers::returnSign($significance) == 1) { return floor($number / $significance) * $significance; } - if (MathTrig::returnSign($number) == -1 && MathTrig::returnSign($significance) == -1) { + if (Helpers::returnSign($number) == -1 && Helpers::returnSign($significance) == -1) { return floor($number / $significance) * $significance; } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/FloorMath.php b/src/PhpSpreadsheet/Calculation/MathTrig/FloorMath.php index cba78a53..8b922829 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/FloorMath.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/FloorMath.php @@ -2,8 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class FloorMath { @@ -23,19 +23,15 @@ class FloorMath */ public static function funcFloorMath($number, $significance = null, $mode = 0) { - MathTrig::nullFalseTrueToNumber($number); - $significance = Functions::flattenSingleValue($significance); - $mode = Functions::flattenSingleValue($mode); - - if ($significance === null) { - $significance = ((float) $number < 0) ? -1 : 1; + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, ($number < 0) ? -1 : 1); + $mode = Helpers::validateNumericNullSubstitution($mode, null); + } catch (Exception $e) { + return $e->getMessage(); } - if (is_numeric($number) && is_numeric($significance) && is_numeric($mode)) { - return self::argsOk((float) $number, (float) $significance, (int) $mode); - } - - return Functions::VALUE(); + return self::argsOk((float) $number, (float) $significance, (int) $mode); } /** @@ -63,6 +59,6 @@ class FloorMath */ private static function floorMathTest(float $number, float $significance, int $mode): bool { - return mathTrig::returnSign($significance) == -1 || (mathTrig::returnSign($number) == -1 && !empty($mode)); + return Helpers::returnSign($significance) == -1 || (Helpers::returnSign($number) == -1 && !empty($mode)); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/FloorPrecise.php b/src/PhpSpreadsheet/Calculation/MathTrig/FloorPrecise.php index 07990aa5..3ce34dc4 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/FloorPrecise.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/FloorPrecise.php @@ -2,8 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class FloorPrecise { @@ -22,14 +22,14 @@ class FloorPrecise */ public static function funcFloorPrecise($number, $significance = 1) { - MathTrig::nullFalseTrueToNumber($number); - $significance = Functions::flattenSingleValue($significance); - - if ((is_numeric($number)) && (is_numeric($significance))) { - return self::argumentsOk((float) $number, (float) $significance); + try { + $number = Helpers::validateNumericNullBool($number); + $significance = Helpers::validateNumericNullSubstitution($significance, null); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + return self::argumentsOk((float) $number, (float) $significance); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php new file mode 100644 index 00000000..63b5082c --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -0,0 +1,87 @@ + 0) ? 1 : -1) : 0; + } + + public static function getEven(float $number): float + { + $significance = 2 * self::returnSign($number); + + return $significance ? (ceil($number / $significance) * $significance) : 0; + } + + /** + * Return NAN or value depending on argument. + * + * @param float $result Number + * + * @return float|string + */ + public static function numberOrNan($result) + { + return is_nan($result) ? Functions::NAN() : $result; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php b/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php index 46784908..e43fe65c 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/IntClass.php @@ -2,8 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; class IntClass { @@ -21,11 +20,12 @@ class IntClass */ public static function funcInt($number) { - MathTrig::nullFalseTrueToNumber($number); - if (is_numeric($number)) { - return (int) floor($number); + try { + $number = Helpers::validateNumericNullBool($number); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + return (int) floor($number); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Mround.php b/src/PhpSpreadsheet/Calculation/MathTrig/Mround.php index 4c040dce..d1b32aa7 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Mround.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Mround.php @@ -2,8 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class Mround { @@ -19,24 +19,22 @@ class Mround */ public static function funcMround($number, $multiple) { - $number = Functions::flattenSingleValue($number); - $number = $number ?? 0; - - $multiple = Functions::flattenSingleValue($multiple); - - if ((is_numeric($number)) && (is_numeric($multiple))) { - if ($number == 0 || $multiple == 0) { - return 0; - } - if ((MathTrig::SIGN($number)) == (MathTrig::SIGN($multiple))) { - $multiplier = 1 / $multiple; - - return round($number * $multiplier) / $multiplier; - } - - return Functions::NAN(); + try { + $number = Helpers::validateNumericNullSubstitution($number, 0); + $multiple = Helpers::validateNumericNullSubstitution($multiple, null); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if ($number == 0 || $multiple == 0) { + return 0; + } + if ((Helpers::returnSign($number)) == (Helpers::returnSign($multiple))) { + $multiplier = 1 / $multiple; + + return round($number * $multiplier) / $multiplier; + } + + return Functions::NAN(); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Odd.php b/src/PhpSpreadsheet/Calculation/MathTrig/Odd.php new file mode 100644 index 00000000..b8ef3dd0 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Odd.php @@ -0,0 +1,38 @@ +getMessage(); + } + + $significance = Helpers::returnSign($number); + if ($significance == 0) { + return 1; + } + + $result = ceil($number / $significance) * $significance; + if ($result == Helpers::getEven($result)) { + $result += $significance; + } + + return $result; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php b/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php index a461001b..05ecb531 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Roman.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Roman @@ -823,31 +824,16 @@ class Roman */ public static function funcRoman($aValue, $style = 0) { - $aValue = Functions::flattenSingleValue($aValue); - self::nullFalseTrueToNumber($aValue); - $style = Functions::flattenSingleValue($style); - if (is_bool($style)) { - $style = $style ? 0 : 4; - } - if (!is_numeric($aValue) || !is_numeric($style)) { - return Functions::VALUE(); + try { + $aValue = Helpers::validateNumericNullBool($aValue); + if (is_bool($style)) { + $style = $style ? 0 : 4; + } + $style = Helpers::validateNumericNullSubstitution($style, null); + } catch (Exception $e) { + return $e->getMessage(); } return self::calculateRoman((int) $aValue, (int) $style); } - - /** - * Many functions accept null/false/true argument treated as 0/0/1. - * - * @param mixed $number - */ - private static function nullFalseTrueToNumber(&$number): void - { - $number = Functions::flattenSingleValue($number); - if ($number === null) { - $number = 0; - } elseif (is_bool($number)) { - $number = (int) $number; - } - } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Round.php b/src/PhpSpreadsheet/Calculation/MathTrig/Round.php index 339f0e27..bc1c6669 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Round.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Round.php @@ -2,8 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; class Round { @@ -19,12 +18,13 @@ class Round */ public static function builtinROUND($number, $precision) { - MathTrig::nullFalseTrueToNumber($number); - - if (!is_numeric($number) || !is_numeric($precision)) { - return Functions::VALUE(); + try { + $number = Helpers::validateNumericNullBool($number); + $precision = Helpers::validateNumericNullBool($precision); + } catch (Exception $e) { + return $e->getMessage(); } - return round($number, $precision); + return round($number, (int) $precision); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/RoundDown.php b/src/PhpSpreadsheet/Calculation/MathTrig/RoundDown.php index ff1f9bcb..bf19d5d5 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/RoundDown.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/RoundDown.php @@ -2,8 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; class RoundDown { @@ -19,21 +18,21 @@ class RoundDown */ public static function funcRoundDown($number, $digits) { - MathTrig::nullFalseTrueToNumber($number); - $digits = Functions::flattenSingleValue($digits); - - if ((is_numeric($number)) && (is_numeric($digits))) { - if ($number == 0.0) { - return 0.0; - } - - if ($number < 0.0) { - return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); - } - - return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); + try { + $number = Helpers::validateNumericNullBool($number); + $digits = Helpers::validateNumericNullSubstitution($digits, null); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if ($number == 0.0) { + return 0.0; + } + + if ($number < 0.0) { + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); + } + + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_UP); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/RoundUp.php b/src/PhpSpreadsheet/Calculation/MathTrig/RoundUp.php index c9507464..a4f00cd3 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/RoundUp.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/RoundUp.php @@ -2,8 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; class RoundUp { @@ -19,21 +18,21 @@ class RoundUp */ public static function funcRoundUp($number, $digits) { - MathTrig::nullFalseTrueToNumber($number); - $digits = Functions::flattenSingleValue($digits); - - if ((is_numeric($number)) && (is_numeric($digits))) { - if ($number == 0.0) { - return 0.0; - } - - if ($number < 0.0) { - return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); - } - - return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); + try { + $number = Helpers::validateNumericNullBool($number); + $digits = Helpers::validateNumericNullSubstitution($digits, null); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if ($number == 0.0) { + return 0.0; + } + + if ($number < 0.0) { + return round($number - 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); + } + + return round($number + 0.5 * 0.1 ** $digits, $digits, PHP_ROUND_HALF_DOWN); } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sec.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sec.php new file mode 100644 index 00000000..9bb5a1b7 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sec.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::verySmallDenominator(1.0, cos($angle)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sech.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sech.php new file mode 100644 index 00000000..191bea4d --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sech.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::verySmallDenominator(1.0, cosh($angle)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php new file mode 100644 index 00000000..84ff523f --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sign.php @@ -0,0 +1,29 @@ +getMessage(); + } + + return Helpers::returnSign($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sin.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sin.php new file mode 100644 index 00000000..f718451c --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sin.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return sin($angle); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sinh.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sinh.php new file mode 100644 index 00000000..ce3ef3e5 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sinh.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return sinh($angle); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Tan.php b/src/PhpSpreadsheet/Calculation/MathTrig/Tan.php new file mode 100644 index 00000000..82612cb8 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Tan.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::verySmallDenominator(sin($angle), cos($angle)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Tanh.php b/src/PhpSpreadsheet/Calculation/MathTrig/Tanh.php new file mode 100644 index 00000000..29bf82e1 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Tanh.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return tanh($angle); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php b/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php index 8cb14d2a..ba82a000 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Trunc.php @@ -2,8 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use Exception; class Trunc { @@ -19,13 +18,13 @@ class Trunc */ public static function funcTrunc($value = 0, $digits = 0) { - MathTrig::nullFalseTrueToNumber($value); - $digits = Functions::flattenSingleValue($digits); - - // Validate parameters - if ((!is_numeric($value)) || (!is_numeric($digits))) { - return Functions::VALUE(); + try { + $value = Helpers::validateNumericNullBool($value); + $digits = Helpers::validateNumericNullSubstitution($digits, null); + } catch (Exception $e) { + return $e->getMessage(); } + $digits = floor($digits); // Truncate diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php index 825626da..9dd6a49d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php @@ -12,19 +12,16 @@ class AcosTest extends TestCase * @dataProvider providerAcos * * @param mixed $expectedResult - * @param mixed $val */ - public function testAcos($expectedResult, $val = null): void + public function testAcos($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=ACOS()'; - } else { - $formula = "=ACOS($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue(0.5); + $sheet->getCell('A1')->setValue("=ACOS($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php index bda64d03..d596cc9e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php @@ -12,19 +12,16 @@ class AcoshTest extends TestCase * @dataProvider providerAcosh * * @param mixed $expectedResult - * @param mixed $val */ - public function testAcosh($expectedResult, $val = null): void + public function testAcosh($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=ACOSH()'; - } else { - $formula = "=ACOSH($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue('1.5'); + $sheet->getCell('A1')->setValue("=ACOSH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php index d81c3b9d..99694215 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class AcotTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerACOT * @@ -21,8 +16,18 @@ class AcotTest extends TestCase */ public function testACOT($expectedResult, $number): void { - $result = MathTrig::ACOT($number); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -5); + $sheet->getCell('A1')->setValue("=ACOT($number)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerACOT() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php index 0a3864cc..1d565e73 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class AcothTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerACOTH * @@ -21,8 +16,18 @@ class AcothTest extends TestCase */ public function testACOTH($expectedResult, $number): void { - $result = MathTrig::ACOTH($number); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -10); + $sheet->getCell('A1')->setValue("=ACOTH($number)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerACOTH() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php index 1edc1c33..c1c836f3 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php @@ -12,19 +12,16 @@ class AsinTest extends TestCase * @dataProvider providerAsin * * @param mixed $expectedResult - * @param mixed $val */ - public function testAsin($expectedResult, $val = null): void + public function testAsin($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=ASIN()'; - } else { - $formula = "=ASIN($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue(0.5); + $sheet->getCell('A1')->setValue("=ASIN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php index 1621eb79..ebbb74f1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php @@ -12,19 +12,16 @@ class AsinhTest extends TestCase * @dataProvider providerAsinh * * @param mixed $expectedResult - * @param mixed $val */ - public function testAsinh($expectedResult, $val = null): void + public function testAsinh($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=ASINH()'; - } else { - $formula = "=ASINH($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue(0.5); + $sheet->getCell('A1')->setValue("=ASINH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php index 4edec4cb..35a96aea 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php @@ -2,28 +2,29 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Atan2Test extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerATAN2 * * @param mixed $expectedResult - * @param mixed $x - * @param mixed $y */ - public function testATAN2($expectedResult, $x, $y): void + public function testATAN2($expectedResult, string $formula): void { - $result = MathTrig::ATAN2($x, $y); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A2')->setValue(5); + $sheet->getCell('A3')->setValue(6); + $sheet->getCell('A1')->setValue("=ATAN2($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerATAN2() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php index 50d76967..4dec2dca 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php @@ -12,19 +12,16 @@ class AtanTest extends TestCase * @dataProvider providerAtan * * @param mixed $expectedResult - * @param mixed $val */ - public function testAtan($expectedResult, $val = null): void + public function testAtan($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=ATAN()'; - } else { - $formula = "=ATAN($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue(5); + $sheet->getCell('A1')->setValue("=ATAN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php index 2863a182..cc8a243f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php @@ -12,19 +12,16 @@ class AtanhTest extends TestCase * @dataProvider providerAtanh * * @param mixed $expectedResult - * @param mixed $val */ - public function testAtan($expectedResult, $val = null): void + public function testAtanh($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=ATANH()'; - } else { - $formula = "=ATANH($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->getCell('A2')->setValue(0.8); + $sheet->getCell('A1')->setValue("=ATANH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php index da7a9a15..d5ada718 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php @@ -12,19 +12,16 @@ class CosTest extends TestCase * @dataProvider providerCos * * @param mixed $expectedResult - * @param mixed $val */ - public function testCos($expectedResult, $val = null): void + public function testCos($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=COS()'; - } else { - $formula = "=COS($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->setCellValue('A2', 2); + $sheet->getCell('A1')->setValue("=COS($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php index 2c452bd5..81dc9c75 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php @@ -12,19 +12,16 @@ class CoshTest extends TestCase * @dataProvider providerCosh * * @param mixed $expectedResult - * @param mixed $val */ - public function testCosh($expectedResult, $val = null): void + public function testCosh($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=COSH()'; - } else { - $formula = "=COSH($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->setCellValue('A2', 2); + $sheet->getCell('A1')->setValue("=COSH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php index 3fee6901..cb009a89 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class CotTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOT * @@ -21,8 +16,18 @@ class CotTest extends TestCase */ public function testCOT($expectedResult, $angle): void { - $result = MathTrig::COT($angle); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -5.2); + $sheet->getCell('A1')->setValue("=COT($angle)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerCOT() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php index e3db23d5..e4b42a4d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class CothTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOTH * @@ -21,8 +16,18 @@ class CothTest extends TestCase */ public function testCOTH($expectedResult, $angle): void { - $result = MathTrig::COTH($angle); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -5.2); + $sheet->getCell('A1')->setValue("=COTH($angle)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerCOTH() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php index 675ebf57..8ae48cde 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class CscTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCSC * @@ -21,8 +16,18 @@ class CscTest extends TestCase */ public function testCSC($expectedResult, $angle): void { - $result = MathTrig::CSC($angle); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -5.2); + $sheet->getCell('A1')->setValue("=CSC($angle)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerCSC() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php index c630be2f..4a7dbc05 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class CschTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCSCH * @@ -21,8 +16,18 @@ class CschTest extends TestCase */ public function testCSCH($expectedResult, $angle): void { - $result = MathTrig::CSCH($angle); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -5.2); + $sheet->getCell('A1')->setValue("=CSCH($angle)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerCSCH() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php index 96c0b046..080925b1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class EvenTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerEVEN * @@ -21,8 +16,14 @@ class EvenTest extends TestCase */ public function testEVEN($expectedResult, $value): void { - $result = MathTrig::EVEN($value); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue("=EVEN($value)"); + $sheet->getCell('A2')->setValue(3.7); + self::assertEquals($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerEVEN() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php index d8b55e7c..45c558cd 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php @@ -16,15 +16,47 @@ class MovedFunctionsTest extends TestCase { public function testMovedFunctions(): void { + self::assertEqualsWithDelta(0, MathTrig::builtinACOS(1), 1E-9); + self::assertEqualsWithDelta(0, MathTrig::builtinACOSH(1), 1E-9); + self::assertEqualsWithDelta(3.04192400109863, MathTrig::ACOT(-10), 1E-9); + self::assertEqualsWithDelta(-0.20273255405408, MathTrig::ACOTH(-5), 1E-9); + self::assertEqualsWithDelta(0, MathTrig::builtinASIN(0), 1E-9); + self::assertEqualsWithDelta(0, MathTrig::builtinASINH(0), 1E-9); + self::assertEqualsWithDelta(0, MathTrig::builtinATAN(0), 1E-9); + self::assertEqualsWithDelta(0, MathTrig::builtinATANH(0), 1E-9); + self::assertEqualsWithDelta('#DIV/0!', MathTrig::ATAN2(0, 0), 1E-9); self::assertEquals(-6, MathTrig::CEILING(-4.5, -2)); + self::assertEquals(1, MathTrig::builtinCOS(0)); + self::assertEquals(1, MathTrig::builtinCOSH(0)); + self::assertEquals('#DIV/0!', MathTrig::COT(0)); + self::assertEquals('#DIV/0!', MathTrig::COTH(0)); + self::assertEquals('#DIV/0!', MathTrig::CSC(0)); + self::assertEquals('#DIV/0!', MathTrig::CSCH(0)); + self::assertEquals(6, MathTrig::EVEN(4.5)); self::assertEquals(-6, MathTrig::FLOOR(-4.5, 2)); self::assertEquals(0.23, MathTrig::FLOORMATH(0.234, 0.01)); self::assertEquals(-4, MathTrig::FLOORPRECISE(-2.5, 2)); self::assertEquals(-9, MathTrig::INT(-8.3)); self::assertEquals(6, MathTrig::MROUND(7.3, 3)); + self::assertEquals(5, MathTrig::ODD(4.5)); self::assertEquals(3.3, MathTrig::builtinROUND(3.27, 1)); self::assertEquals(662, MathTrig::ROUNDDOWN(662.79, 0)); self::assertEquals(663, MathTrig::ROUNDUP(662.79, 0)); + self::assertEquals(1, MathTrig::SEC(0)); + self::assertEquals(1, MathTrig::SECH(0)); + self::assertEquals(1, MathTrig::SIGN(79.2)); + self::assertEquals(0, MathTrig::builtinSIN(0)); + self::assertEquals(0, MathTrig::builtinSINH(0)); + self::assertEquals(0, MathTrig::builtinTAN(0)); + self::assertEquals(0, MathTrig::builtinTANH(0)); self::assertEquals(70, MathTrig::TRUNC(79.2, -1)); + self::assertEquals(1, MathTrig::returnSign(79.2)); + self::assertEquals(80, MathTrig::getEven(79.2)); + $nullVal = null; + MathTrig::nullFalseTrueToNumber($nullVal); + self::assertSame(0, $nullVal); + $nullVal = true; + MathTrig::nullFalseTrueToNumber($nullVal); + self::assertSame(1, $nullVal); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php index 6c5758c6..ed262d9c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class OddTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerODD * @@ -21,8 +16,14 @@ class OddTest extends TestCase */ public function testODD($expectedResult, $value): void { - $result = MathTrig::ODD($value); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->getCell('A1')->setValue("=ODD($value)"); + $sheet->getCell('A2')->setValue(3.7); + self::assertEquals($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerODD() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php index ad4b196c..a47ae7b5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class SecTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSEC * @@ -21,8 +16,18 @@ class SecTest extends TestCase */ public function testSEC($expectedResult, $angle): void { - $result = MathTrig::SEC($angle); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -5.2); + $sheet->getCell('A1')->setValue("=SEC($angle)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerSEC() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php index b9488bda..65ed7b73 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class SechTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSECH * @@ -21,8 +16,18 @@ class SechTest extends TestCase */ public function testSECH($expectedResult, $angle): void { - $result = MathTrig::SECH($angle); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 2.7); + $sheet->setCellValue('A4', -3.8); + $sheet->setCellValue('A5', -5.2); + $sheet->getCell('A1')->setValue("=SECH($angle)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerSECH() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php index 68f5acb9..a4311219 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php @@ -2,17 +2,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class SignTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSIGN * @@ -21,8 +16,17 @@ class SignTest extends TestCase */ public function testSIGN($expectedResult, $value): void { - $result = MathTrig::SIGN($value); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 1.3); + $sheet->setCellValue('A3', 0); + $sheet->setCellValue('A4', -3.8); + $sheet->getCell('A1')->setValue("=SIGN($value)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); } public function providerSIGN() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php index 7a144e0e..e9ad6329 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php @@ -12,19 +12,16 @@ class SinTest extends TestCase * @dataProvider providerSin * * @param mixed $expectedResult - * @param mixed $val */ - public function testSin($expectedResult, $val = null): void + public function testSin($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=SIN()'; - } else { - $formula = "=SIN($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->setCellValue('A2', 2); + $sheet->getCell('A1')->setValue("=SIN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php index c24bb192..38bfc7ef 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php @@ -12,19 +12,16 @@ class SinhTest extends TestCase * @dataProvider providerCosh * * @param mixed $expectedResult - * @param mixed $val */ - public function testSinh($expectedResult, $val = null): void + public function testSinh($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=SINH()'; - } else { - $formula = "=SINH($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->setCellValue('A2', 2); + $sheet->getCell('A1')->setValue("=SINH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php index 13093f6a..5a482cd8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php @@ -12,19 +12,16 @@ class TanTest extends TestCase * @dataProvider providerTan * * @param mixed $expectedResult - * @param mixed $val */ - public function testTan($expectedResult, $val = null): void + public function testTan($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=TAN()'; - } else { - $formula = "=TAN($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->setCellValue('A2', 1); + $sheet->getCell('A1')->setValue("=TAN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php index 69f28e8a..5fe50d7c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php @@ -12,19 +12,16 @@ class TanhTest extends TestCase * @dataProvider providerTanh * * @param mixed $expectedResult - * @param mixed $val */ - public function testTanh($expectedResult, $val = null): void + public function testTanh($expectedResult, string $formula): void { - if ($val === null) { + if ($expectedResult === 'exception') { $this->expectException(CalcExp::class); - $formula = '=TANH()'; - } else { - $formula = "=TANH($val)"; } $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); + $sheet->setCellValue('A2', 1); + $sheet->getCell('A1')->setValue("=TANH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/data/Calculation/MathTrig/ACOS.php b/tests/data/Calculation/MathTrig/ACOS.php index f592de09..487a0628 100644 --- a/tests/data/Calculation/MathTrig/ACOS.php +++ b/tests/data/Calculation/MathTrig/ACOS.php @@ -1,10 +1,14 @@ Date: Sat, 13 Mar 2021 12:43:16 +0100 Subject: [PATCH 36/89] Add null typehint to Worksheet::getColumnDimension() since it returns null (#1914) * Add null typehint to `Worksheet::getColumnDimension()` since it can return null * `getColumnDimensionByColumn()` and `getRowDimension()` --- src/PhpSpreadsheet/Worksheet/Worksheet.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 19119970..09ce3e61 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -1337,7 +1337,7 @@ class Worksheet implements IComparable * @param int $pRow Numeric index of the row * @param bool $create * - * @return RowDimension + * @return null|RowDimension */ public function getRowDimension($pRow, $create = true) { @@ -1363,7 +1363,7 @@ class Worksheet implements IComparable * @param string $pColumn String index of the column eg: 'A' * @param bool $create * - * @return ColumnDimension + * @return null|ColumnDimension */ public function getColumnDimension($pColumn, $create = true) { @@ -1390,7 +1390,7 @@ class Worksheet implements IComparable * * @param int $columnIndex Numeric column coordinate of the cell * - * @return ColumnDimension + * @return null|ColumnDimension */ public function getColumnDimensionByColumn($columnIndex) { From 0edfc3257f9f2e530e04d8e8b8acd9dd2a2accb4 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Date: Sun, 14 Mar 2021 20:34:23 +0530 Subject: [PATCH 37/89] Update the fopen mode for writer --- src/PhpSpreadsheet/Writer/BaseWriter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpSpreadsheet/Writer/BaseWriter.php b/src/PhpSpreadsheet/Writer/BaseWriter.php index afda5c43..d52b0d4a 100644 --- a/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -108,7 +108,7 @@ abstract class BaseWriter implements IWriter return; } - $fileHandle = $filename ? fopen($filename, 'wb+') : false; + $fileHandle = $filename ? fopen($filename, 'wb') : false; if ($fileHandle === false) { throw new Exception('Could not open file "' . $filename . '" for writing.'); } From 5686453bcce92a31fcbe566c087bf063f0bf9941 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Date: Sun, 14 Mar 2021 20:48:10 +0530 Subject: [PATCH 38/89] Add test case for excel with media --- .../Writer/Xlsx/DrawingsTest.php | 22 ++++++++++++++++++ .../XLSX/saving_drawing_with_same_path.xlsx | Bin 0 -> 11717 bytes 2 files changed, 22 insertions(+) create mode 100644 tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index d6ad77c6..58e3be57 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -42,4 +42,26 @@ class DrawingsTest extends AbstractFunctional // Fake assert. The only thing we need is to ensure the file is loaded without exception self::assertNotNull($reloadedSpreadsheet); } + + /** + * Test save and load XLSX file with drawing with the same file name. + */ + public function testSaveLoadWithDrawingWithSamePath(): void + { + // Read spreadsheet from file + $filePath = 'tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx'; + $reader = new Xlsx(); + $spreadsheet = $reader->load($filePath); + + $spreadsheet->getActiveSheet()->setCellValue('D5', 'foo'); + // Save spreadsheet to file to the same path. Success test case won't + // throw exception here + $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); + $writer->save($filePath); + + $reloadedSpreadsheet = $reader->load($filePath); + + // Fake assert. The only thing we need is to ensure the file is loaded without exception + self::assertNotNull($reloadedSpreadsheet); + } } diff --git a/tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx b/tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..53f9a6b9a46d3a7358770e05e5cd2a6c9e40fa10 GIT binary patch literal 11717 zcmb_?1zgly)BjS^r6AoP(kb2D4We|Hbcu8$-O`|x3P^W{l(dwfwDh`k$Ga;RFW2YZ z|I2%y|L|FUyPt*cIcMfOXXeZtC0Q6)EC590!faH2zWL+V9RMjn&%xxGBa^xsDgb`% zSYH47<@_8O00p}b1pxf~U!|WI2ml0t=nV{6XaE5D7X~8)=_kl~ue+W<2J<$l`w9NX(jYd;$;YnsX2dk#QzrKX-lNzni@u8W77Nc?d}&CQ zp>ttDDHp%PRP*h6s%7+sF3uQq#!6whXY=uxv zib`%Om$CyWS$ps~`xpH5Lb5Ce4zu=WEq6--<9Zn#4p_XUNi_YGHEAl!qsxg%Q^Rc~ zcGKbIq@J;usfS<0IMUNHL$j^4j--yjQIPWo!5zNQ429dOZ=K7)8IUMpiKv7ph?)r@ zi4QP-AmV`?#OzVvM#*Vj+#Y_WxigAn@Q>wpbp_{BhiY(kRrT`>3kKHar7h;AJ zG<5@el+uVMh`h%t_VNZL6;D;Ayq=z;ye_@NrK2LKd5`}%v|LZpKO%NFPey{5y?A&> zdPQbHwvugV?g8Eg621u4EP`|J(BLyO0-8oBQXm%gcjZSr#;Dw=IiEzV7AF(dXIQZ}7Pl+rk-ep>O8H}<} z9!^3Y8QfStd&J?x+aJ`t&2q#gr_)nfMBLH} zc_uc@bw(XBjz%w4e_nlVO974(em9;=Qw3tl zzjuC2@Hdz@ZER%gU~(HH=f)~4wlQM`&Yok->h+t$TlTR^kOxsq%XO?;Su-hpnaXSA zIRF89aNjf?SEQwXEcCA46DXQ;7SW2QzXpD_3T>4%*1w3OvFBxXpgU$ z>nUt_7|UbO`+%h@N)c%(!i8cWvNyD4-p02ch)#Y9ZDAovWz-y z%u&0kBo%n|E#b+zNpo?d58Bl+N#O#KEQeMtIC@uU!saY5SA17JWXF-Tki8|MmZN9f zAHk;P;XR7OMcp;qb3Wd}dMuyt?nSUTHFNTMK-c~l4q{M7{FVp@ut6ba`_uQ)!v7jmVZa7`{_HBC zs-4Q~U%ze@Q1`)s0{sfW)z-nv(AL)KHhhSPl~p7V1_#x1?CjcCID%Tg=v=L*O{T{lRl?Rq1i6 zOJ{iR#iF5)f*|zT?#I+)j^$J5@I#p$v-ogqyAL zhQ)RZE6MfV7FX|Nwb3^jPrpB6x=3KO|WA6!FBZpLtMDW?<+KG!& zm1Rbex~)qztr}9G(3mV8=3~~4Qs%VRd^_siDq)1I?zqfCdVttTlLuN!p%2|i*7FN4 zR9FC4C;;v3T+kQ6wmOvl}lbi*=CreQx-et zNpHT`=^B7LOC$*j)qEf3|CFua zrFi#Bya_3(D(C8%$jIyiFhS@%4n%Hgtittv+6x}+#zeJ|dN{%k=l~P z`QfSeTifAVV-PcxLV^k@CcNp45noaS9&I+EFd(!*DRMP;F^xPLXC$JO8ciFqnh58g z0_J{u2okq?#gRH0RkwS#Tffs9Ot6dO?)zq9UQ%kt3j3MqBLe{A(a=EOVgFYTwiou} zxulBigN@Dxtm#i82L$33!bKH#CrwzU&$xL51!h^Pq9)%e@X;y1OZq6o;<%z4rXk>{ zH|xMekY9rH&>?YKx~iMzQx8lDu4-p3CT%9`ljM^?X%HF#mF83KH!7PXH`R|$j9GpMlfSInRCoe3jsH({X-aHxg{*(0PV8TOQ zzmeWZAKS=G-%x)`UQS~RT_Z%v2Ra%Th8lwas+0i3{|H;C51=|v+TsJl6%+s~oQ%z^ zvkI9BJOXk^3)ds?*pTu6+|7T_bKly#~ z5RwXyZ|>oj{Eh|6xmp;Z1q9bBvji<5swOi|maZB{E|H-(j(F9&5UYE(yOK@rumr2q zI}3`941>yjN}4yjYkO3&;dCifmeI9hvb6jU<sq+vl7NsM1=>riQPEfsIq z)2Buu)A*sYygw1$RXRw^P(hZ*pqjFhf4oy>=pf9;+A7=Lh#MHOlIm=2M>gIxz>e@_ zgiVZ@))n!|g5kP>6)BQ4iCT4#{Pyv<(R7z*-26_~PECn33GnTP*KU8Mj-_h7Z8Imk z)J3bw@G-Oq-zn(4nmaxc6>)Y|5(uQ43A0)~lEq~v+n(fSD48|vT4va(89Vi8JJFS^ zMO&dVR`jQDUD}PA9^-0B2TN97%6 zWe%)C$1FbHfa>?Rmna*^)Oi&KZFR>b>GSKkL)?0gYzHRk(TbM)G>Fcgs4pm*%4D7u z)N*Ki?)61pS||EMR8E|_C`Cs1jWSrr(tMJ%L^C(uW?S*?F=_L#)fki4aE)63wgyj> zYQ;H4*(HSO3wmiY$4~$OggO7rbjX7@$^U@)Z>kE>U|g&}7;bLh zU}CK5PSQ=(VKI zZYwEidxZtdD#LDQA+a2u9LsOw!eHMkeb5p9 zwlKJ*x_{pZc;Nv6+$%jnlEpvvr=Jg&+vqAnUEXnu8Kd#=vAG9&5XU(JjPXbwd$M$} zoJdM}$#_+0p)(a;@Wsi8C3Lw}GU^J&nf&U4d=r;rm4{U@MpWx=?069rcX))!oH?R0 z#y^!aVU|Z)wPIT)NxGK|e_!}kC{bR??PDayTuG&9Ue2!Hx=Ff|Rki!j^+Q@`sl3C( z8X^P46c-hCI9IF``l|SLaeZ2~GcUdiEFA6iL<7s2HeUQrMZZ*Q^$bjV z>5tYqu{I>^5`|KE)yWd=oS}?<0o%q|v6&;yI@Bx-rcm{qVR#w}(x`_)U*3}npB878 zJlGH;+9ECg>PD*Dw;-S>RYwS7H6=}#d>pr?enjrEB`abLZ?$~1jEO~wCYE*JGEHi8 zzRVzOteR?c-Y#V?Y1wT#pf==#uIVl6EE*VH5_%k{Y0BW{6`t#Q7kIoOFyhy{Wvl-Eiuir(Cax* zO_pn1=l%)&n565MmYqx5OmvJ!JKA!DwC{)@Nq zyiQh^xVq*17S6qrI*bvcHtIpit!4hjQ%l5dd+BJVyV89AnO}|7XjP*}2A=P68|K-I zYx2^!uN!r6tPQQU@ z+)|r4me?(N9R<}I_b}}qIrl=KQrKq%%r|c7OY5yEiT!J+UyIhhuU_mL%uRTzMcdh!y)Z~+BzTOAq4OeFBh4&qh+?6!aGOE>)O zzuD>7!Q=hm!N0PCKi=HRgVR-}6$n@`JVB2&y_j@Wpp@fLMa0w+Fe2XXKy~t_R<5U- zel58D0|kbZ<)_Lb zLqwh0m9*%ay1Ojw+ohpw`-Y{h*MsF{3@~tlE*1Z+F(i^Kv=Vag(zKrt{0|ZZWU?+(v$N1e+Wk2J?h2`PGvlk@;*zf)Fk*7co--Ji^E9nnLc$Z9)@6l%oAkp2= zu;rKy%MNSA~R8KkYkW70-s7nfF&&hO(>_Dn4zB!UkpKwk+@7QK^22%Qs)k7cCe@-BFZ&d>1y(RDrr}*l!50 zX5(=c&RHtlrMhO3{G2b>IO*uUu7Ck8~n72CT%LT`>gs@YQGz!@heW(>V#Rjow8eZ2tgYi&0pF zrP`#O+-pp>hMe*~a$h-=j3P1m@u2tf{WF>u`)57d?Pgu*-U42Gt4H&SciITJ5S`mz z&3ax;aAsx+W_q3OY`-#X7~O6@KUynp_At0ZOLVkQK;10pzO%3~cyV-53e`llB^8Wr zAxBv!9yufs)ym-yV79PPxJ`?$ebaljH zhVS1()!h4tJ;%6G_$hEWEnP0rW`G^BzwM)17tqMVJoXze=F|myV7i<`?}?xoA3iKPa(;Eo?7)i;RcQx!6o%z(cI4r}B@+fQ#v6JKZk`S90?B0=%kQor}!;t5`T&_w| zAN|&<^fKIF)}*{96f0Cgt~)i!k*Kzxx)Gu4ow8Yq7|($yw=YBJF!5svs^^bH%UC*~ zs4M52WMA^=J)M1RU_3+mrB^LU#c#_}U|xMlqhM}dbX?r5`qqJ8mv z)M3sHRBy9?p@_x?(e0fgoil2z)@e5#)iuz6teUY}dA2as=`M%ItHdT@)7uat z%glt^Vl2@8^1i1Reyb792R~%Efy|h>b))xh{1Ng=JA0@C%8ZF*;vEpHkqZ)Cy~K0% z9R*hUK?y#cmapJHI>|QMGEOdKE3NxI#q%#mveI$mHBf-!%VzIlligKFFtwTqXb|Rd zPo67$ggjiL%uFG?eCXH8M~_2HjyLZU&*Bgs#8t#rS9JcO*kWW{%a$5QFFf+G_R&HE za)^o%mCS)9PgxF+h-6m6#2)?9Vuew}>lME)E;uh%I`yPL>8>4hi(ad^os;u z)DIz2fojv55^(BG?A}Ci^!v6iY?gz=SXz`K2J*xUfM;P(C#gkcJB=0!Qj))yXfLXY zcL-wXuA-;S%w~)?8)-T7G**N3|>+8P$fayI6PV4nlYt|5Ry>5@s-Myu>I8f;pkXlPny+ zif%O#diUV;H5s&5&INPIShdV5r|e>WBDtzDq{fN~E=@tnpGOBLxU>`pY+jfNSgO|KWo;rOabgQ-9aM`pp$|)jMc!?dXc%SuY|Fw8oj#pnk5}Mb74bAE zOXnUa?3utLvuevU&$=2;@X2|r(zUJtIEGbO873$2^q4Iy>WBZPbq$Gpa_mR=|46>EM!Ak=~jFZfrn<|@S8tSR5JTiXim)?$c( z&XGzFpSnDip^0&%@zvHkgbApql6xj?&S`yqs7@e7i0~Rdv!ie_xl3xJ|Bx$0QEn8z zIEz*WV^zMS5rpNzK7DG2(J`gP4;zK_fO2xwb%T?TeTB%ulQKr0kH)TVTU>?}O;TPw zWOY}bVB{_Lr_r9b{kpCXADbLge*Oxxj;vT(jx;i$L7GCd;N2iPeQ-z5pF4c@SSEln zBdCuFKb^-IPEyC1Bzj`;v23T~%tyzQOg^*%Efn-HjX5Wg(*YX6t%jG!NB8>{W102H zfIVb8cuVXj;{9Fk@Z7c+*MHWk0w_y0Al+Js+Wualv*6Iqc%{&*gPW0wp(#6;fuZ3e zc9uu1Y^)|6#@x)zrbY&)rltm5kJwlk?QG0`HFCH9Xp8?=!rDODa$NZyyI`oVKexYU zhX_5=?4ud!k0|9Lu6PWe!-dR89)^c|6zRjfsAer-apz9w%M8XyGHl108M{s1!j?r0 z^SyP@32zLe0y%%>kVO(22$vWTRPune^A|u7Zu1-4~8wz9jhuY^R^`w z2HhKVO?Cwt^-B9)KfjvQ+I}xmZ-0#XWg<1y#d|RQVDpTiRgv*&Bn!#MqEDBgZ^?J_ zwgeXD2P##G1(GZBM-AsctRL0%U;YrVeeudm{Mq6qEna(aCdhLdo2+?7GeSwyqdPux zTacRORX^_u|3Kr!$>c!1_q|N-I<=KEh8SS5u>v;?q4rVxS&njrcwrR`{p#BKO4$I- zYeX=c;1On-mE#t`{DE-j@7YBQ{)Xa;UH@^u`14eME5D5x{DjoTi~%>)sE284J5q-c zllLuTsAv=&H+Bss6<*q757aNAY&Y+)%xYGnzRS#3uN)Frepf~pKv9sEu}M1rm1u%j zpXK!!7q9Hy`=LXl1?ysLc^>z=dL$Du38=zpjvFzQBaE3HR(h6mTwC>fjc@}bom+i7 z@*Y-^d*Ca1Cl`+06)BL_|F*D3GQEGEvpP3A7A-HRi$JuM5oU6?T}T>wrT7~!y_>1< zXu{EZO~IdMFm7Re;&QMDLNxLBIuiI(N57@dH_uJC)R8>o{si0{e}0K+RC$D&f6@=h9U7=_M6G?kUG-K-UBD+8SKSy#^QwVR4{e!j_k(XPQq! zsPG~&jC75%@9qwB^KfLPbMLZ>0V<++n}*ek`)}Sx@ZBfA7nqoo#6Rw0N_Y1?dIe?B z2i})w(>~t^jByyq^qkN)U&!u}@Zv9GHHz)8(ToZ{2-vl*_f29@M@&OjoO-QPk+K-D z!9#p%&xPVNqNrnrg>PhskIt{y>ZfUHe|WG1^D`a@ue|L8D-WU~C0QtFESP`3^aK90 zpVA@>dEEB=afNvuWo}+>fm{N*mKuL2-23imK@&@6tJiK zyC&83AP}(M2?*&hT}z8FIqJ{p-`M|us^(fPkmo-v0D|#5_D$8a2-{p?|JpZii+s(% zn|%aG2kKgKd~2?dZ{Lsle<|dz9*KhSQz5r)OI*N$2!Jlpexwm<<#tErAUQ3Q|+ZF5$-u;Fit}?Fzef3YtGIc9`)i!* zy28IA{p|~S*DShr*qa9$$T{#@a(owwelFl@-ZwYme-*HArnZpd(Y56GvXk6k>;IR- z((h}$nKD9>=WEIFjV1egvi(Wyf3;Z08)?>G2dpChJ67Ce;4KG)y8~GTKKQ2r9L;tq HAfNsRwQ3$l literal 0 HcmV?d00001 From 51abdf0b8f7b406bab07e4d954e0e8454a8f1f8f Mon Sep 17 00:00:00 2001 From: Vivek Kumar Date: Sun, 14 Mar 2021 22:20:11 +0530 Subject: [PATCH 39/89] Refactor xlsx writer * Move file handler creation and file addition to the end --- src/PhpSpreadsheet/Writer/BaseWriter.php | 2 +- src/PhpSpreadsheet/Writer/Xlsx.php | 93 ++++++++++-------- .../Writer/Xlsx/DrawingsTest.php | 1 + .../XLSX/saving_drawing_with_same_path.xlsx | Bin 11717 -> 7936 bytes 4 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/BaseWriter.php b/src/PhpSpreadsheet/Writer/BaseWriter.php index d52b0d4a..afda5c43 100644 --- a/src/PhpSpreadsheet/Writer/BaseWriter.php +++ b/src/PhpSpreadsheet/Writer/BaseWriter.php @@ -108,7 +108,7 @@ abstract class BaseWriter implements IWriter return; } - $fileHandle = $filename ? fopen($filename, 'wb') : false; + $fileHandle = $filename ? fopen($filename, 'wb+') : false; if ($fileHandle === false) { throw new Exception('Could not open file "' . $filename . '" for writing.'); } diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index d71541c8..10fc50c9 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -182,8 +182,6 @@ class Xlsx extends BaseWriter $this->pathNames = []; $this->spreadSheet->garbageCollect(); - $this->openFileHandle($pFilename); - $saveDebugLog = Calculation::getInstance($this->spreadSheet)->getDebugLog()->getWriteDebugLog(); Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog(false); $saveDateReturnType = Functions::getReturnDateType(); @@ -206,77 +204,72 @@ class Xlsx extends BaseWriter // Create drawing dictionary $this->drawingHashTable->addFromSource($this->getWriterPart('Drawing')->allDrawings($this->spreadSheet)); - $options = new Archive(); - $options->setEnableZip64(false); - $options->setOutputStream($this->fileHandle); - - $this->zip = new ZipStream(null, $options); - + $zipContent = []; // Add [Content_Types].xml to ZIP file - $this->addZipFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts)); + $zipContent['[Content_Types].xml'] = $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts); //if hasMacros, add the vbaProject.bin file, Certificate file(if exists) if ($this->spreadSheet->hasMacros()) { $macrosCode = $this->spreadSheet->getMacrosCode(); if ($macrosCode !== null) { // we have the code ? - $this->addZipFile('xl/vbaProject.bin', $macrosCode); //allways in 'xl', allways named vbaProject.bin + $zipContent['xl/vbaProject.bin'] = $macrosCode; //allways in 'xl', allways named vbaProject.bin if ($this->spreadSheet->hasMacrosCertificate()) { //signed macros ? // Yes : add the certificate file and the related rels file - $this->addZipFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate()); - $this->addZipFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet)); + $zipContent['xl/vbaProjectSignature.bin'] = $this->spreadSheet->getMacrosCertificate(); + $zipContent['xl/_rels/vbaProject.bin.rels'] = $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet); } } } //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'); - $this->addZipFile($tmpRibbonTarget, $this->spreadSheet->getRibbonXMLData('data')); + $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) { - $this->addZipFile($tmpRootPath . $aPath, $aContent); + $zipContent[$tmpRootPath . $aPath] = $aContent; } //the rels for files - $this->addZipFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet)); + $zipContent[$tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels'] = $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet); } } // Add relationships to ZIP file - $this->addZipFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet)); - $this->addZipFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet)); + $zipContent['_rels/.rels'] = $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet); + $zipContent['xl/_rels/workbook.xml.rels'] = $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet); // Add document properties to ZIP file - $this->addZipFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet)); - $this->addZipFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet)); + $zipContent['docProps/app.xml'] = $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet); + $zipContent['docProps/core.xml'] = $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet); $customPropertiesPart = $this->getWriterPart('DocProps')->writeDocPropsCustom($this->spreadSheet); if ($customPropertiesPart !== null) { - $this->addZipFile('docProps/custom.xml', $customPropertiesPart); + $zipContent['docProps/custom.xml'] = $customPropertiesPart; } // Add theme to ZIP file - $this->addZipFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet)); + $zipContent['xl/theme/theme1.xml'] = $this->getWriterPart('Theme')->writeTheme($this->spreadSheet); // Add string table to ZIP file - $this->addZipFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable)); + $zipContent['xl/sharedStrings.xml'] = $this->getWriterPart('StringTable')->writeStringTable($this->stringTable); // Add styles to ZIP file - $this->addZipFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet)); + $zipContent['xl/styles.xml'] = $this->getWriterPart('Style')->writeStyles($this->spreadSheet); // Add workbook to ZIP file - $this->addZipFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); + $zipContent['xl/workbook.xml'] = $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas); $chartCount = 0; // Add worksheets for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { - $this->addZipFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); + $zipContent['xl/worksheets/sheet' . ($i + 1) . '.xml'] = $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts); if ($this->includeCharts) { $charts = $this->spreadSheet->getSheet($i)->getChartCollection(); if (count($charts) > 0) { foreach ($charts as $chart) { - $this->addZipFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas)); + $zipContent['xl/charts/chart' . ($chartCount + 1) . '.xml'] = $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas); ++$chartCount; } } @@ -287,19 +280,19 @@ class Xlsx extends BaseWriter // Add worksheet relationships (drawings, ...) for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { // Add relationships - $this->addZipFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); + $zipContent['xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels'] = $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts); // Add unparsedLoadedData $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); $unparsedLoadedData = $this->spreadSheet->getUnparsedLoadedData(); if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['ctrlProps'] as $ctrlProp) { - $this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']); + $zipContent[$ctrlProp['filePath']] = $ctrlProp['content']; } } if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['printerSettings'] as $ctrlProp) { - $this->addZipFile($ctrlProp['filePath'], $ctrlProp['content']); + $zipContent[$ctrlProp['filePath']] = $ctrlProp['content']; } } @@ -312,13 +305,13 @@ class Xlsx extends BaseWriter // Add drawing and image relationship parts if (($drawingCount > 0) || ($chartCount > 0)) { // Drawing relationships - $this->addZipFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); + $zipContent['xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels'] = $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts); // Drawings - $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts); } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) { // Drawings - $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $zipContent['xl/drawings/drawing' . ($i + 1) . '.xml'] = $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts); } // Add unparsed drawings @@ -327,7 +320,7 @@ class Xlsx extends BaseWriter $drawingFile = array_search($relId, $unparsedLoadedData['sheets'][$sheetCodeName]['drawingOriginalIds']); if ($drawingFile !== false) { $drawingFile = ltrim($drawingFile, '.'); - $this->addZipFile('xl' . $drawingFile, $drawingXml); + $zipContent['xl' . $drawingFile] = $drawingXml; } } } @@ -335,30 +328,30 @@ class Xlsx extends BaseWriter // Add comment relationship parts if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) { // VML Comments - $this->addZipFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i))); + $zipContent['xl/drawings/vmlDrawing' . ($i + 1) . '.vml'] = $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i)); // Comments - $this->addZipFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i))); + $zipContent['xl/comments' . ($i + 1) . '.xml'] = $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i)); } // Add unparsed relationship parts if (isset($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'])) { foreach ($unparsedLoadedData['sheets'][$sheetCodeName]['vmlDrawings'] as $vmlDrawing) { - $this->addZipFile($vmlDrawing['filePath'], $vmlDrawing['content']); + $zipContent[$vmlDrawing['filePath']] = $vmlDrawing['content']; } } // Add header/footer relationship parts if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) { // VML Drawings - $this->addZipFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); + $zipContent['xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml'] = $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i)); // VML Drawing relationships - $this->addZipFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); + $zipContent['xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels'] = $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i)); // Media foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) { - $this->addZipFile('xl/media/' . $image->getIndexedFilename(), file_get_contents($image->getPath())); + $zipContent['xl/media/' . $image->getIndexedFilename()] = file_get_contents($image->getPath()); } } } @@ -381,7 +374,7 @@ class Xlsx extends BaseWriter $imageContents = file_get_contents($imagePath); } - $this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + $zipContent['xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename())] = $imageContents; } elseif ($this->getDrawingHashTable()->getByIndex($i) instanceof MemoryDrawing) { ob_start(); call_user_func( @@ -391,13 +384,23 @@ class Xlsx extends BaseWriter $imageContents = ob_get_contents(); ob_end_clean(); - $this->addZipFile('xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename()), $imageContents); + $zipContent['xl/media/' . str_replace(' ', '_', $this->getDrawingHashTable()->getByIndex($i)->getIndexedFilename())] = $imageContents; } } Functions::setReturnDateType($saveDateReturnType); Calculation::getInstance($this->spreadSheet)->getDebugLog()->setWriteDebugLog($saveDebugLog); + $this->openFileHandle($pFilename); + + $options = new Archive(); + $options->setEnableZip64(false); + $options->setOutputStream($this->fileHandle); + + $this->zip = new ZipStream(null, $options); + + $this->addZipFiles($zipContent); + // Close file try { $this->zip->finish(); @@ -545,4 +548,12 @@ class Xlsx extends BaseWriter $this->zip->addFile($path, $content); } } + + private function addZipFiles(array $zipContent): void + { + foreach ($zipContent as $path => $content) + { + $this->addZipFile($path, $content); + } + } } diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index 58e3be57..0880bde9 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Settings; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; diff --git a/tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx b/tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx index 53f9a6b9a46d3a7358770e05e5cd2a6c9e40fa10..a88d43d3dc71f029d530ce805b217e50183b414f 100644 GIT binary patch delta 2615 zcmZ{m2UJs67{^}-fr>N7OaBEC?JA$^d@*pcx{vO?mH*%`+eWL-@X6)Pix>wdvkAh zq_i>wL7zYtX>^kA@?K0F0)j4}AV>-P8tBBJv#E4;h|jTDDl6C|J|?=dAHhO=Dha99 zSXfSgV0S49S`Y3aOlmaC)I>C9^Cs0LLsN7s{6wL3AXE*4zQ(KbZSa#uk|F(_(l64$ zE7%|*2n|BR7>B%>j98W_B{o)Ee+qdDVJu0Qs5SAJfWQC(BS{=$FsWjs3&lp5N+PW6 z`w`E>X(>#EPIWsbL4$%t(4T zOGIILyX9KU(9p~s?UzIJSDM<93Fs5a_L^0J>Jc0oXWN_OV{y2&Ki_?7I_>GgkCjco zhH|yNpSSc|^H|cr<>qf)@MGE@TTZwYDcanMD!;}p+tP7(5=BhUXZGyz8;#O8unK6M za}HkLN(|S2upU3Km|~?bBaBOkVdwI0KGcf$Y!1wGwhfqFaxMFA7P)qDM?)?RFM~cs zNJEhP$M8WSJT>P5{TnlM+UrEQ2Y;pA%O+K0&^xm$H!1vq*e2P8py?@-GLYmI zSV6^=Bu5!c+Cc_`Chpp(Y&l}hk}HvaXf^;VQz;8UI0)j8U^a1iz5~q1X6VEja>QHw z2IEL|nfq04k=ywS`6+})EdhtH{*(K*3z8Dab10>`CVsW+@S~QUwEGG z=FuJ3gzsc$UOhm%S4^qtaQpea&DKHEa~JtpU2^`sqwSWqo0re|1dfz+I9g}ik>4W} zymz*o4Q%P-^mY9C$BwOgH=lBxYB-WnyLj=nG*K|HLFm9!RxC-g?#33A%(yU~w-)BwNV|K02Eo?%S{NR%{UH6rBABBOdu->V7 z8}aHXtIOf1y}4}{3K?}{-4)HoJAB{BOg>J1>IpFB1ZtDlSP{w#H{FmQ>y zaDK|=y94%y`za-ydF>9v8f!w>)S&hw`|cw5Mx~HuQ@WqB;em5qUUy&X9lREG{Wj#kG$V2D*q(mePSao?m9i%}cfywyKVuYS6a4Qve&c-P!y74kjy`@n~n2?=Z&>hk?;y;m(((<@ZKsg=lOkJu#B$kpY&G|po zd!5ZrAx~_-@V-;$*}n3swig?EEj#gAH^M!Ghy1(0ulH#D^5RZAt0DZ1TGEg;o7dw` z3(hJFt@3K~vp^8WZ-i{vq2u;d`{0=u7~zM33GP}MTkQ_TR|4f30u|~2LEX~Q<|R)mchgfba@{|3{KCZBsiTW7b+tHUc92wYVR0PAcQz?8adUF+>EzUfcg>^`4Q9y zPgXn_=8-F)xOA12$T1_~rQnXaLTG|pFX#u{KjAdoJy+=r{_lFqVDqh=7~DEn;|;+H z{~tyQPXM>k)dVWQmEfkz6}bvL1>DJ2Q}ljJ@sF>;QMiAtqRYU}Ky^ts1F+$mZ2>d0 z6!+&rQHY*tDB zwljas{#p*80apsim#XPtF9;xCPgxULuaGc)0Kqek0DzgbOblLt7cgSOu;c;&{;wDq z7-iEx(BlRG5q222z(6HVh%&f& z4k28Syd8IV`V%0gFwnx@KtZ3N*tc*GppZD)qGnkz>+ zBFvW=4)!0ciL4h#;V=%}r7#m9G=M_h8xTOAWhe(A=fuR^>td`CIR{g8emjUMgDAo{ z-@L$1kp~R`0?ZvS#&)iD!H^SXf`P>oexJv6a93;6J*?6!C@56+>rh=#o0{0 zBsp8Wu?gAu8($vbP(qnv2dL)3_p4(3YCRMvd9Ess;!^qFX?b*Rc%W4p33-Zhc@-q zo+L*@s_RpVrc4P~E}=p>{?4NM#=2WKxyvQ(uGOMc^VjdnoQ}5`5|U94ctfkWVV3+$ z$l>6Kk7u5GwTXET8j-1#)e7%YiR+3e5uYzRM0|nn_uZ=4V_BvQ1cSd(pqo+M()IQ} z;$S2STeD5z%f%#M>eKIOQL6}+NM52^SAH54>TpmKEJ3(P!v&T--ou~@DFMa1tE)B6 z2yf@43FRin_50BGhQc0x%?o{ZqL{lEaW8S$+RAZQ*zph{iYrZ$d##8JqC!R_c2W#` z8?bwhZiwREoDc`{;4;OuT>4-xNM8O!4VyPYo5KXYRg3xkL+D$wQ@u%~U{32N1o~Ys z0%!BugW?wIw{}Oj^`nnlFjUNsWv@cFUi^BlI3FLfx)8Po@w600S+4 za=2+gcMy)4E~EIoCX1l&YsxYjY9IU3#Pyhy9@zYt!TJ5wQvy56ynjxRjUr zLQFJCaj4qL`+1_kXs~3a$#{C_c9C_P&eHfYWQA+dA+%RnIb&sKdTrZiLWhp(M(XyS zpeOO1-uX!8301Lzh{Z#r36eK#6{-(}&H$goV@Z$ukvx3dIQ01Z*w8})fFyvX522*M zrACM&4%XBzEs^^7KYK?AC+L3S<@RVV*2%O;wA!An)l!qLlebFAvcQc%PAD5Xj zsk_U;b*Jq6vsT^j`o!udSJw)hKlRzB726%cxHoFu zf}dQGDOs6uac7K)+F)gK(qwmO%W9C8R34M<=a7dAviQrd@TmVP9n&)N;tQFkO`8s5 zV&dL;6dcWOzwEMBTf-Akw<$@=(Z)jXeZs@jXxxYM1?6kr7|RW#8INe_J-53J@#Yt0 zht_+dbIpFHzjl~h9WiXV-499bo$N)4DhU}EOzi&~?fGC(tOwC_e3Mq%I1H0XLWA%SCV(U0S|l@?P_n(s3_=j@)um4WFtrwpp#GFGt3k zY_KZVtjzC@+yK^KGA}t*+qS3TTp#)E)A zhqzcb?HGl03RyE{T{GyO9o1{v4I+H>d$I##{y4qjlXTHup|tyB*YJkEcfUl0F*F+q z{MCtqxC_spZ8VCN*VgISpLkRJYQ--}y#%fEvX~+Y@j&S@yU-Lmg~eyX*T-VLpV*Ds8$3VosoA=`*4 zElrmqRt14>!4c&d5M6lb!k~t;po;QJOBe1|Ynvu5k!8;eO3FP`a|^A-2s!>Dwiile ziF;G!k|xcnabpu<7Ruu4X857-tf`NKp%W&xHIhnWm$*%FCWA5Q!v`ZHbE47@<6g*f zTh#1~9v#uT+?G-m(%ft7CJ6GGTyZ2H5I<3RL|n zkAFw?w0KBpD7Xd-4;-)@Tcs!cLbTp^{Pnu~+t$R5`qf2eXgKiY@Z-DwAXH|(hbyVD|(%X=GU?)IhB7#eldvP@neQ(AZ zaPS+a5zWM8WgfdUht)mH3Zsnyz-bnA5wSm4*byw}BC`u}SlY9!FggK2-zg)uuQFxYOSp&`qO8p;D^-P{1%LVLu92WL0D~!%($Ax4*CsP)SH4B|xio=?oWrbYP zv2=p0=nuJpT`s^tu!3h<1I`{Q)g0-0_KO1jZP}$bEa_QR7@f{UoG0-AQQxzlAEwJL zeBh~{Wrfjsd@x;5^LvHE68Q59(|l-l-2w|ImK8=H5`gl7GZ_Vb-1fzjrN9jt*^ z)__|rIOA`&<~LJXSx?m#>?hx0!_zR+*uB$$^Jjv8ZCIQ_v*M0j+Q1r#Wrfi-OypU` z1ak0*jpcY4W4=q~W}<+|j7vC`N{~0eMGD+pTstQ2zpHWVq5~dSSXLOFB?8dfW&fY0 zC>%B808W()GaQsym&QJHL30p`C{P%E1x0_0lD7QXyIDcDNQ%HK=ie0j(Hn5<>38KM ztbR1A*oUpT1mnc$cnN7H;6FV6`=?NXXEc^I;6f$nIC0S*X2?Ed&22l^CFp6gVjMGM z-3j}E1 Date: Sun, 14 Mar 2021 22:25:13 +0530 Subject: [PATCH 40/89] Update PHPCS changes --- src/PhpSpreadsheet/Writer/Xlsx.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index 10fc50c9..23354f8c 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -551,8 +551,7 @@ class Xlsx extends BaseWriter private function addZipFiles(array $zipContent): void { - foreach ($zipContent as $path => $content) - { + foreach ($zipContent as $path => $content) { $this->addZipFile($path, $content); } } From ed62526acad6dc0d8da3a5f14e27d207379ce1e3 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 14 Mar 2021 19:58:10 +0100 Subject: [PATCH 41/89] First step extracting INDIRECT() and OFFSET() to their own classes (#1921) * First step extracting INDIRECT() and OFFSET() to their own classes * Start building unit tests for OFFSET() and INDEX() * Named ranges should be handled by the Calculation Engine, not by the implementation of the Excel INDIRECT() function * When calling the calculation engine to get the range of cells to return, INDIRECT() and OFFSET() should use the instance of the calculation engine for the current workbook to benefit from cached results in that range There's a couple of minor bugfixes in here; but it's basically just refactoring of the INDIRECT() and OFFSET() Excel functions into their own classes - still needs a lot of work on unit testing; and there's a lot more that could be improved in the code itself (including handling of the a1 flag for R1C1 format in INDIRECT() --- samples/Calculations/LookupRef/INDIRECT.php | 33 ++++ samples/Calculations/LookupRef/OFFSET.php | 33 ++++ .../Calculation/Calculation.php | 6 +- src/PhpSpreadsheet/Calculation/LookupRef.php | 153 ++++-------------- .../Calculation/LookupRef/Indirect.php | 75 +++++++++ .../Calculation/LookupRef/Offset.php | 136 ++++++++++++++++ .../Functions/LookupRef/IndirectTest.php | 55 +++++++ .../Functions/LookupRef/OffsetTest.php | 32 ++++ tests/data/Calculation/LookupRef/INDIRECT.php | 16 ++ tests/data/Calculation/LookupRef/OFFSET.php | 8 + 10 files changed, 426 insertions(+), 121 deletions(-) create mode 100644 samples/Calculations/LookupRef/INDIRECT.php create mode 100644 samples/Calculations/LookupRef/OFFSET.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php create mode 100644 src/PhpSpreadsheet/Calculation/LookupRef/Offset.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php create mode 100644 tests/data/Calculation/LookupRef/INDIRECT.php create mode 100644 tests/data/Calculation/LookupRef/OFFSET.php diff --git a/samples/Calculations/LookupRef/INDIRECT.php b/samples/Calculations/LookupRef/INDIRECT.php new file mode 100644 index 00000000..ffbada9a --- /dev/null +++ b/samples/Calculations/LookupRef/INDIRECT.php @@ -0,0 +1,33 @@ +log('Returns the cell specified by a text string.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$data = [ + [8, 9, 0], + [3, 4, 5], + [9, 1, 3], + [4, 6, 2], +]; +$worksheet->fromArray($data, null, 'C1'); + +$spreadsheet->addNamedRange(new NamedRange('NAMED_RANGE_FOR_CELL_D4', $worksheet, '="$D$4"')); + +$worksheet->getCell('A1')->setValue('=INDIRECT("C1")'); +$worksheet->getCell('A2')->setValue('=INDIRECT("D"&4)'); +$worksheet->getCell('A3')->setValue('=INDIRECT("E"&ROW())'); +$worksheet->getCell('A4')->setValue('=SUM(INDIRECT("$C$4:$E$4"))'); +$worksheet->getCell('A5')->setValue('=INDIRECT(NAMED_RANGE_FOR_CELL_D4)'); + +for ($row = 1; $row <= 5; ++$row) { + $cell = $worksheet->getCell("A{$row}"); + $helper->log("A{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/samples/Calculations/LookupRef/OFFSET.php b/samples/Calculations/LookupRef/OFFSET.php new file mode 100644 index 00000000..ae613ec5 --- /dev/null +++ b/samples/Calculations/LookupRef/OFFSET.php @@ -0,0 +1,33 @@ +log('Returns a cell range that is a specified number of rows and columns from a cell or range of cells.'); + +// Create new PhpSpreadsheet object +$spreadsheet = new Spreadsheet(); +$worksheet = $spreadsheet->getActiveSheet(); + +$data = [ + [null, 'Week 1', 'Week 2', 'Week 3', 'Week 4'], + ['Sunday', 4500, 2200, 3800, 1500], + ['Monday', 5500, 6100, 5200, 4800], + ['Tuesday', 7000, 6200, 5000, 7100], + ['Wednesday', 8000, 4000, 3900, 7600], + ['Thursday', 5900, 5500, 6900, 7100], + ['Friday', 4900, 6300, 6900, 5200], + ['Saturday', 3500, 3900, 5100, 4100], +]; +$worksheet->fromArray($data, null, 'A3'); + +$worksheet->getCell('H1')->setValue('=OFFSET(A3, 3, 1)'); +$worksheet->getCell('H2')->setValue('=SUM(OFFSET(A3, 3, 1, 1, 4))'); +$worksheet->getCell('H3')->setValue('=SUM(OFFSET(B3:E3, 3, 0))'); +$worksheet->getCell('H4')->setValue('=SUM(OFFSET(E3, 1, -3, 7))'); + +for ($row = 1; $row <= 4; ++$row) { + $cell = $worksheet->getCell("H{$row}"); + $helper->log("H{$row}: {$cell->getValue()} => {$cell->getCalculatedValue()}"); +} diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 1db4722b..c88eff31 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1408,7 +1408,7 @@ class Calculation ], 'INDIRECT' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'INDIRECT'], + 'functionCall' => [LookupRef\Indirect::class, 'INDIRECT'], 'argumentCount' => '1,2', 'passCellReference' => true, ], @@ -1881,7 +1881,7 @@ class Calculation ], 'OFFSET' => [ 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, - 'functionCall' => [LookupRef::class, 'OFFSET'], + 'functionCall' => [LookupRef\Offset::class, 'OFFSET'], 'argumentCount' => '3-5', 'passCellReference' => true, 'passByReference' => [true], @@ -2702,7 +2702,7 @@ class Calculation * Get an instance of this class. * * @param ?Spreadsheet $spreadsheet Injected spreadsheet for working with a PhpSpreadsheet Spreadsheet object, - * or NULL to create a standalone claculation engine + * or NULL to create a standalone calculation engine */ public static function getInstance(?Spreadsheet $spreadsheet = null): self { diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index d2cc4f94..17115a06 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -4,12 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Address; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\HLookup; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Indirect; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Lookup; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix; +use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Offset; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\RowColumnInformation; use PhpOffice\PhpSpreadsheet\Calculation\LookupRef\VLookup; use PhpOffice\PhpSpreadsheet\Cell\Cell; -use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; class LookupRef @@ -181,56 +182,22 @@ class LookupRef * Excel Function: * =INDIRECT(cellAddress) * + * @Deprecated 1.18.0 + * + * @see Use the INDIRECT() method in the LookupRef\Indirect class instead + * * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010 * * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula) * @param Cell $pCell The current cell (containing this formula) * - * @return mixed The cells referenced by cellAddress + * @return array|string An array containing a cell or range of cells, or a string on error * * @TODO Support for the optional a1 parameter introduced in Excel 2010 */ public static function INDIRECT($cellAddress = null, ?Cell $pCell = null) { - $cellAddress = Functions::flattenSingleValue($cellAddress); - if ($cellAddress === null || $cellAddress === '') { - return Functions::REF(); - } - - $cellAddress1 = $cellAddress; - $cellAddress2 = null; - if (strpos($cellAddress, ':') !== false) { - [$cellAddress1, $cellAddress2] = explode(':', $cellAddress); - } - - if ( - (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress1, $matches)) || - (($cellAddress2 !== null) && (!preg_match('/^' . Calculation::CALCULATION_REGEXP_CELLREF . '$/i', $cellAddress2, $matches))) - ) { - if (!preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/i', $cellAddress1, $matches)) { - return Functions::REF(); - } - - if (strpos($cellAddress, '!') !== false) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - $sheetName = trim($sheetName, "'"); - $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName); - } else { - $pSheet = $pCell->getWorksheet(); - } - - return Calculation::getInstance()->extractNamedRange($cellAddress, $pSheet, false); - } - - if (strpos($cellAddress, '!') !== false) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - $sheetName = trim($sheetName, "'"); - $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName); - } else { - $pSheet = $pCell->getWorksheet(); - } - - return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false); + return Indirect::INDIRECT($cellAddress, $pCell); } /** @@ -243,87 +210,33 @@ class LookupRef * Excel Function: * =OFFSET(cellAddress, rows, cols, [height], [width]) * - * @param null|string $cellAddress The reference from which you want to base the offset. Reference must refer to a cell or - * range of adjacent cells; otherwise, OFFSET returns the #VALUE! error value. - * @param mixed $rows The number of rows, up or down, that you want the upper-left cell to refer to. - * Using 5 as the rows argument specifies that the upper-left cell in the reference is - * five rows below reference. Rows can be positive (which means below the starting reference) - * or negative (which means above the starting reference). - * @param mixed $columns The number of columns, to the left or right, that you want the upper-left cell of the result - * to refer to. Using 5 as the cols argument specifies that the upper-left cell in the - * reference is five columns to the right of reference. Cols can be positive (which means - * to the right of the starting reference) or negative (which means to the left of the - * starting reference). - * @param mixed $height The height, in number of rows, that you want the returned reference to be. Height must be a positive number. - * @param mixed $width The width, in number of columns, that you want the returned reference to be. Width must be a positive number. + * @Deprecated 1.18.0 * - * @return string A reference to a cell or range of cells + * @see Use the OFFSET() method in the LookupRef\Offset class instead + * + * @param null|string $cellAddress The reference from which you want to base the offset. + * Reference must refer to a cell or range of adjacent cells; + * otherwise, OFFSET returns the #VALUE! error value. + * @param mixed $rows The number of rows, up or down, that you want the upper-left cell to refer to. + * Using 5 as the rows argument specifies that the upper-left cell in the + * reference is five rows below reference. Rows can be positive (which means + * below the starting reference) or negative (which means above the starting + * reference). + * @param mixed $columns The number of columns, to the left or right, that you want the upper-left cell + * of the result to refer to. Using 5 as the cols argument specifies that the + * upper-left cell in the reference is five columns to the right of reference. + * Cols can be positive (which means to the right of the starting reference) + * or negative (which means to the left of the starting reference). + * @param mixed $height The height, in number of rows, that you want the returned reference to be. + * Height must be a positive number. + * @param mixed $width The width, in number of columns, that you want the returned reference to be. + * Width must be a positive number. + * + * @return array|string An array containing a cell or range of cells, or a string on error */ public static function OFFSET($cellAddress = null, $rows = 0, $columns = 0, $height = null, $width = null, ?Cell $pCell = null) { - $rows = Functions::flattenSingleValue($rows); - $columns = Functions::flattenSingleValue($columns); - $height = Functions::flattenSingleValue($height); - $width = Functions::flattenSingleValue($width); - if ($cellAddress === null) { - return 0; - } - - if (!is_object($pCell)) { - return Functions::REF(); - } - - $sheetName = null; - if (strpos($cellAddress, '!')) { - [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); - $sheetName = trim($sheetName, "'"); - } - if (strpos($cellAddress, ':')) { - [$startCell, $endCell] = explode(':', $cellAddress); - } else { - $startCell = $endCell = $cellAddress; - } - [$startCellColumn, $startCellRow] = Coordinate::coordinateFromString($startCell); - [$endCellColumn, $endCellRow] = Coordinate::coordinateFromString($endCell); - - $startCellRow += $rows; - $startCellColumn = Coordinate::columnIndexFromString($startCellColumn) - 1; - $startCellColumn += $columns; - - if (($startCellRow <= 0) || ($startCellColumn < 0)) { - return Functions::REF(); - } - $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; - if (($width != null) && (!is_object($width))) { - $endCellColumn = $startCellColumn + $width - 1; - } else { - $endCellColumn += $columns; - } - $startCellColumn = Coordinate::stringFromColumnIndex($startCellColumn + 1); - - if (($height != null) && (!is_object($height))) { - $endCellRow = $startCellRow + $height - 1; - } else { - $endCellRow += $rows; - } - - if (($endCellRow <= 0) || ($endCellColumn < 0)) { - return Functions::REF(); - } - $endCellColumn = Coordinate::stringFromColumnIndex($endCellColumn + 1); - - $cellAddress = $startCellColumn . $startCellRow; - if (($startCellColumn != $endCellColumn) || ($startCellRow != $endCellRow)) { - $cellAddress .= ':' . $endCellColumn . $endCellRow; - } - - if ($sheetName !== null) { - $pSheet = $pCell->getWorksheet()->getParent()->getSheetByName($sheetName); - } else { - $pSheet = $pCell->getWorksheet(); - } - - return Calculation::getInstance()->extractCellRange($cellAddress, $pSheet, false); + return Offset::OFFSET($cellAddress, $rows, $columns, $height, $width, $pCell); } /** @@ -370,6 +283,10 @@ class LookupRef * Excel Function: * =MATCH(lookup_value, lookup_array, [match_type]) * + * @Deprecated 1.18.0 + * + * @see Use the MATCH() method in the LookupRef\ExcelMatch class instead + * * @param mixed $lookupValue The value that you want to match in lookup_array * @param mixed $lookupArray The range of cells being searched * @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below. diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php new file mode 100644 index 00000000..690b32e4 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -0,0 +1,75 @@ +getParent() : null) + ->extractCellRange($cellAddress, $pSheet, false); + } + + private static function extractWorksheet($cellAddress, Cell $pCell): array + { + $sheetName = ''; + if (strpos($cellAddress, '!') !== false) { + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + $sheetName = trim($sheetName, "'"); + } + + $pSheet = ($sheetName !== '') + ? $pCell->getWorksheet()->getParent()->getSheetByName($sheetName) + : $pCell->getWorksheet(); + + return [$cellAddress, $pSheet]; + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php new file mode 100644 index 00000000..25ec498b --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Offset.php @@ -0,0 +1,136 @@ +getParent() : null) + ->extractCellRange($cellAddress, $pSheet, false); + } + + private static function extractWorksheet($cellAddress, Cell $pCell): array + { + $sheetName = ''; + if (strpos($cellAddress, '!') !== false) { + [$sheetName, $cellAddress] = Worksheet::extractSheetTitle($cellAddress, true); + $sheetName = trim($sheetName, "'"); + } + + $pSheet = ($sheetName !== '') + ? $pCell->getWorksheet()->getParent()->getSheetByName($sheetName) + : $pCell->getWorksheet(); + + return [$cellAddress, $pSheet]; + } + + private static function adjustEndCellColumnForWidth(string $endCellColumn, $width, int $startCellColumn, $columns) + { + $endCellColumn = Coordinate::columnIndexFromString($endCellColumn) - 1; + if (($width !== null) && (!is_object($width))) { + $endCellColumn = $startCellColumn + (int) $width - 1; + } else { + $endCellColumn += (int) $columns; + } + + return $endCellColumn; + } + + private static function adustEndCellRowForHeight($height, int $startCellRow, $rows, $endCellRow): int + { + if (($height !== null) && (!is_object($height))) { + $endCellRow = $startCellRow + (int) $height - 1; + } else { + $endCellRow += (int) $rows; + } + + return $endCellRow; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php new file mode 100644 index 00000000..f1018e7f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/IndirectTest.php @@ -0,0 +1,55 @@ +getMockBuilder(Calculation::class) +// ->setMethods(['getInstance', 'extractCellRange']) +// ->disableOriginalConstructor() +// ->getMock(); +// $calculation->method('getInstance') +// ->willReturn($calculation); +// $calculation->method('extractCellRange') +// ->willReturn([]); +// +// $worksheet = $this->getMockBuilder(Cell::class) +// ->setMethods(['getParent']) +// ->disableOriginalConstructor() +// ->getMock(); +// +// $cell = $this->getMockBuilder(Cell::class) +// ->setMethods(['getWorksheet']) +// ->disableOriginalConstructor() +// ->getMock(); +// $cell->method('getWorksheet') +// ->willReturn($worksheet); + + $result = LookupRef::INDIRECT($cellReference); + self::assertSame($expectedResult, $result); + } + + public function providerINDIRECT() + { + return require 'tests/data/Calculation/LookupRef/INDIRECT.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php new file mode 100644 index 00000000..631af08d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/OffsetTest.php @@ -0,0 +1,32 @@ + Date: Sun, 14 Mar 2021 12:04:07 -0700 Subject: [PATCH 42/89] Coverage for Helper/Samples (#1920) * Coverage for Helper/Samples I was perplexed by the fact that Helper/Samples seemed to be entirely uncovered when running the test suite, since I know all the samples are run as part of the test. I think that what must be happening is that the Helper code is invoked mostly as part of a Data Provider (and therefore not counted), not as part of the test proper (which would count). So, this change adds a small number of tests which result in Samples being 100% covered. Covering one statement was tricky - simulating the inability to create a test directory. Mocking, a technique I have not used before, solves this problem admirably. * Suggestions From Mark Baker Tests changed from assertEquals to assertSame. Added @covers annotation to test class. Validate parameter for method being mocked. --- src/PhpSpreadsheet/Helper/Sample.php | 13 ++++--- .../Helper/SampleCoverageTest.php | 39 +++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Helper/SampleCoverageTest.php diff --git a/src/PhpSpreadsheet/Helper/Sample.php b/src/PhpSpreadsheet/Helper/Sample.php index a91b195e..c84c3930 100644 --- a/src/PhpSpreadsheet/Helper/Sample.php +++ b/src/PhpSpreadsheet/Helper/Sample.php @@ -71,7 +71,7 @@ class Sample /** * Returns an array of all known samples. * - * @return string[] [$name => $path] + * @return string[][] [$name => $path] */ public function getSamples() { @@ -132,6 +132,11 @@ class Sample $this->logEndingNotes(); } + protected function isDirOrMkdir(string $folder): bool + { + return \is_dir($folder) || \mkdir($folder); + } + /** * Returns the temporary directory and make sure it exists. * @@ -140,10 +145,8 @@ class Sample private function getTemporaryFolder() { $tempFolder = sys_get_temp_dir() . '/phpspreadsheet'; - if (!is_dir($tempFolder)) { - if (!mkdir($tempFolder) && !is_dir($tempFolder)) { - throw new RuntimeException(sprintf('Directory "%s" was not created', $tempFolder)); - } + if (!$this->isDirOrMkdir($tempFolder)) { + throw new RuntimeException(sprintf('Directory "%s" was not created', $tempFolder)); } return $tempFolder; diff --git a/tests/PhpSpreadsheetTests/Helper/SampleCoverageTest.php b/tests/PhpSpreadsheetTests/Helper/SampleCoverageTest.php new file mode 100644 index 00000000..f3f616fe --- /dev/null +++ b/tests/PhpSpreadsheetTests/Helper/SampleCoverageTest.php @@ -0,0 +1,39 @@ +getSamples(); + self::assertArrayHasKey('Basic', $samples); + $basic = $samples['Basic']; + self::assertArrayHasKey('02 Types', $basic); + self::assertSame('Basic/02_Types.php', $basic['02 Types']); + self::assertSame('phpunit', $helper->getPageTitle()); + self::assertSame('

phpunit

', $helper->getPageHeading()); + } + + public function testDirectoryFail(): void + { + $this->expectException(RuntimeException::class); + + $helper = $this->getMockBuilder(Sample::class) + ->onlyMethods(['isDirOrMkdir']) + ->getMock(); + $helper->expects(self::once()) + ->method('isDirOrMkdir') + ->with(self::isType('string')) + ->willReturn(false); + self::assertSame('', $helper->getFilename('a.xlsx')); + } +} From d99a4a3fac622298010ceca2796bdc476801ad13 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 14 Mar 2021 12:04:50 -0700 Subject: [PATCH 43/89] Improve Coverage of BIN2DEC etc. (#1902) * Improve Coverage of BIN2DEC etc. The following functions have some special handling depending on the Calculation mode: - BIN2DEC - BIN2HEX - BIN2OCT - DEC2BIN - DEC2HEX - DEC2OCT - HEX2BIN - HEX2DEC - HEX2OCT - OCT2BIN - OCT2DEC - OCT2HEX Ods accepts boolean for its numeric argument. This had already been coded, but there were no tests for it. Gnumeric allows the use of non-integer argument where Excel/Ods do not. The existing code allowed this for certain functions but not for others. Gnumeric consistently allows it, so there is no need for parameter gnumericCheck in convertBase::ValidateValue. Again, there were no tests for this. There were some minor changes needed: - In functions where you are allowed to specify the numnber of "places" in the result, there is an upper bound of 10 which had not been enforced. - Negative values were not handled correctly in some cases. - There was at least one (avoidable) error on a 32-bit system. - Some upper and lower bounds were not being enforced. In addition to enforcing those, the bounds are now defined as class constants in ConvertDecimal. Many tests have been added, so that Engineering is now almost 100% covered. The exception is some BESSEL code. There have been some recent changes to BESSEL which are not yet part of my fork, so I could not address those now. However, I freely admit that, when I looked at the uncovered portion, it seemed like it might be a difficult task, so I probably wouldn't have tackled it anyhow. In particular, the uncovered code seemed to deal with very large numbers, and, although PhpSpreadsheet and Excel both give very large results for these conditions, their answers are not particularly close to each other. I think we're dealing with resuts approaching infinity. More study is needed. --- .../Calculation/Engineering/ConvertBase.php | 10 +- .../Calculation/Engineering/ConvertBinary.php | 18 +-- .../Engineering/ConvertDecimal.php | 51 ++++++-- .../Calculation/Engineering/ConvertHex.php | 7 +- .../Calculation/Engineering/ConvertOctal.php | 6 +- .../Functions/Engineering/Bin2DecTest.php | 67 +++++++++- .../Functions/Engineering/Bin2HexTest.php | 67 +++++++++- .../Functions/Engineering/Bin2OctTest.php | 67 +++++++++- .../Functions/Engineering/Dec2BinTest.php | 67 +++++++++- .../Functions/Engineering/Dec2HexTest.php | 73 ++++++++++- .../Functions/Engineering/Dec2OctTest.php | 67 +++++++++- .../Functions/Engineering/Hex2BinTest.php | 70 ++++++++++- .../Functions/Engineering/Hex2DecTest.php | 70 ++++++++++- .../Functions/Engineering/Hex2OctTest.php | 67 +++++++++- .../Engineering/MovedFunctionsTest.php | 31 +++++ .../Functions/Engineering/Oct2BinTest.php | 67 +++++++++- .../Functions/Engineering/Oct2DecTest.php | 67 +++++++++- .../Functions/Engineering/Oct2HexTest.php | 67 +++++++++- .../data/Calculation/Engineering/BIN2DEC.php | 67 ++++------ .../data/Calculation/Engineering/BIN2HEX.php | 101 +++++---------- .../data/Calculation/Engineering/BIN2OCT.php | 106 +++++----------- .../data/Calculation/Engineering/DEC2BIN.php | 119 +++++------------- .../data/Calculation/Engineering/DEC2HEX.php | 104 +++++---------- .../data/Calculation/Engineering/DEC2OCT.php | 76 ++++------- .../data/Calculation/Engineering/HEX2BIN.php | 96 +++++--------- .../data/Calculation/Engineering/HEX2DEC.php | 86 ++++--------- .../data/Calculation/Engineering/HEX2OCT.php | 80 ++++-------- .../data/Calculation/Engineering/OCT2BIN.php | 84 ++++--------- .../data/Calculation/Engineering/OCT2DEC.php | 53 +++----- .../data/Calculation/Engineering/OCT2HEX.php | 60 ++++----- 30 files changed, 1179 insertions(+), 792 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedFunctionsTest.php diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php index 5122e011..a095690d 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertBase.php @@ -7,7 +7,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; class ConvertBase { - protected static function validateValue($value, bool $gnumericCheck = false): string + protected static function validateValue($value): string { if (is_bool($value)) { if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE) { @@ -16,8 +16,10 @@ class ConvertBase $value = (int) $value; } - if ($gnumericCheck && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - $value = floor((float) $value); + if (is_numeric($value)) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + $value = floor((float) $value); + } } return strtoupper((string) $value); @@ -30,7 +32,7 @@ class ConvertBase } if (is_numeric($places)) { - if ($places < 0) { + if ($places < 0 || $places > 10) { throw new Exception(Functions::NAN()); } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php index 57f9fc41..ff4873ac 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertBinary.php @@ -25,7 +25,7 @@ class ConvertBinary extends ConvertBase public static function toDecimal($value): string { try { - $value = self::validateValue(Functions::flattenSingleValue($value), true); + $value = self::validateValue(Functions::flattenSingleValue($value)); $value = self::validateBinary($value); } catch (Exception $e) { return $e->getMessage(); @@ -65,7 +65,7 @@ class ConvertBinary extends ConvertBase public static function toHex($value, $places = null): string { try { - $value = self::validateValue(Functions::flattenSingleValue($value), true); + $value = self::validateValue(Functions::flattenSingleValue($value)); $value = self::validateBinary($value); $places = self::validatePlaces(Functions::flattenSingleValue($places)); } catch (Exception $e) { @@ -73,8 +73,11 @@ class ConvertBinary extends ConvertBase } if (strlen($value) == 10) { - // Two's Complement - return str_repeat('F', 8) . substr(strtoupper(dechex((int) bindec(substr($value, -9)))), -2); + $high2 = substr($value, 0, 2); + $low8 = substr($value, 2); + $xarr = ['00' => '00000000', '01' => '00000001', '10' => 'FFFFFFFE', '11' => 'FFFFFFFF']; + + return $xarr[$high2] . strtoupper(substr('0' . dechex((int) bindec($low8)), -2)); } $hexVal = (string) strtoupper(dechex((int) bindec($value))); @@ -105,16 +108,15 @@ class ConvertBinary extends ConvertBase public static function toOctal($value, $places = null): string { try { - $value = self::validateValue(Functions::flattenSingleValue($value), true); + $value = self::validateValue(Functions::flattenSingleValue($value)); $value = self::validateBinary($value); $places = self::validatePlaces(Functions::flattenSingleValue($places)); } catch (Exception $e) { return $e->getMessage(); } - if (strlen($value) == 10) { - // Two's Complement - return str_repeat('7', 7) . substr(strtoupper(decoct((int) bindec(substr($value, -9)))), -3); + if (strlen($value) == 10 && substr($value, 0, 1) === '1') { // Two's Complement + return str_repeat('7', 6) . strtoupper(decoct((int) bindec("11$value"))); } $octVal = (string) decoct((int) bindec($value)); diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php index bd249633..a34332fb 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertDecimal.php @@ -7,6 +7,13 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; class ConvertDecimal extends ConvertBase { + const LARGEST_OCTAL_IN_DECIMAL = 536870911; + const SMALLEST_OCTAL_IN_DECIMAL = -536870912; + const LARGEST_BINARY_IN_DECIMAL = 511; + const SMALLEST_BINARY_IN_DECIMAL = -512; + const LARGEST_HEX_IN_DECIMAL = 549755813887; + const SMALLEST_HEX_IN_DECIMAL = -549755813888; + /** * toBinary. * @@ -43,16 +50,13 @@ class ConvertDecimal extends ConvertBase } $value = (int) floor((float) $value); - if ($value < -512 || $value > 511) { + if ($value > self::LARGEST_BINARY_IN_DECIMAL || $value < self::SMALLEST_BINARY_IN_DECIMAL) { return Functions::NAN(); } $r = decbin($value); // Two's Complement $r = substr($r, -10); - if (strlen($r) >= 11) { - return Functions::NAN(); - } return self::nbrConversionFormat($r, $places); } @@ -92,16 +96,37 @@ class ConvertDecimal extends ConvertBase return $e->getMessage(); } - $value = (int) floor((float) $value); - $r = strtoupper(dechex($value)); - if (strlen($r) == 8) { - // Two's Complement - $r = 'FF' . $r; + $value = floor((float) $value); + if ($value > self::LARGEST_HEX_IN_DECIMAL || $value < self::SMALLEST_HEX_IN_DECIMAL) { + return Functions::NAN(); } + $r = strtoupper(dechex((int) $value)); + $r = self::hex32bit($value, $r); return self::nbrConversionFormat($r, $places); } + public static function hex32bit(float $value, string $hexstr, bool $force = false): string + { + if (PHP_INT_SIZE === 4 || $force) { + if ($value >= 2 ** 32) { + $quotient = (int) ($value / (2 ** 32)); + + return strtoupper(substr('0' . dechex($quotient), -2) . $hexstr); + } + if ($value < -(2 ** 32)) { + $quotient = 256 - (int) ceil((-$value) / (2 ** 32)); + + return strtoupper(substr('0' . dechex($quotient), -2) . substr("00000000$hexstr", -8)); + } + if ($value < 0) { + return "FF$hexstr"; + } + } + + return $hexstr; + } + /** * toOctal. * @@ -138,11 +163,11 @@ class ConvertDecimal extends ConvertBase } $value = (int) floor((float) $value); - $r = decoct($value); - if (strlen($r) == 11) { - // Two's Complement - $r = substr($r, -10); + if ($value > self::LARGEST_OCTAL_IN_DECIMAL || $value < self::SMALLEST_OCTAL_IN_DECIMAL) { + return Functions::NAN(); } + $r = decoct($value); + $r = substr($r, -10); return self::nbrConversionFormat($r, $places); } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php index 9147bf08..cbf155ed 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertHex.php @@ -42,7 +42,9 @@ class ConvertHex extends ConvertBase return $e->getMessage(); } - return ConvertDecimal::toBinary(self::toDecimal($value), $places); + $dec = self::toDecimal($value); + + return ConvertDecimal::toBinary($dec, $places); } /** @@ -129,9 +131,6 @@ class ConvertHex extends ConvertBase } $decimal = self::toDecimal($value); - if ($decimal < -536870912 || $decimal > 536870911) { - return Functions::NAN(); - } return ConvertDecimal::toOctal($decimal, $places); } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php b/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php index 1e8823ad..872f0b70 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ConvertOctal.php @@ -127,14 +127,16 @@ class ConvertOctal extends ConvertBase return $e->getMessage(); } - $hexVal = strtoupper(dechex((int) self::toDecimal((int) $value))); + $hexVal = strtoupper(dechex((int) self::toDecimal($value))); + $hexVal = (PHP_INT_SIZE === 4 && strlen($value) === 10 && $value[0] >= '4') ? "FF$hexVal" : $hexVal; return self::nbrConversionFormat($hexVal, $places); } protected static function validateOctal(string $value): string { - if (strlen($value) > preg_match_all('/[01234567]/', $value)) { + $numDigits = (int) preg_match_all('/[01234567]/', $value); + if (strlen($value) > $numDigits || $numDigits > 10) { throw new Exception(Functions::NAN()); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2DecTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2DecTest.php index faba3de8..bcefa891 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2DecTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2DecTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Bin2DecTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerBIN2DEC * * @param mixed $expectedResult + * @param mixed $formula */ - public function testBIN2DEC($expectedResult, ...$args): void + public function testBin2Dec($expectedResult, $formula): void { - $result = Engineering::BINTODEC(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=BIN2DEC($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Bin2DecTest extends TestCase { return require 'tests/data/Calculation/Engineering/BIN2DEC.php'; } + + /** + * @dataProvider providerBIN2DEC + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testBIN2DECOds($expectedResult, $formula): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=BIN2DEC($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testBIN2DECFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=BIN2DEC(101.1)'); + self::assertEquals(5, $sheet->getCell($cell)->getCalculatedValue(), 'Gnumeric'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=BIN2DEC(101.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue(), 'Ods'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=BIN2DEC(101.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue(), 'Excel'); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2HexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2HexTest.php index 2a16d5ac..2cbea34a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2HexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2HexTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Bin2HexTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerBIN2HEX * * @param mixed $expectedResult + * @param mixed $formula */ - public function testBIN2HEX($expectedResult, ...$args): void + public function testBin2Hex($expectedResult, $formula): void { - $result = Engineering::BINTOHEX(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=BIN2HEX($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Bin2HexTest extends TestCase { return require 'tests/data/Calculation/Engineering/BIN2HEX.php'; } + + /** + * @dataProvider providerBIN2HEX + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testBIN2HEXOds($expectedResult, $formula): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=BIN2HEX($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testBIN2HEXFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=BIN2HEX(101.1)'); + self::assertEquals(5, $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=BIN2HEX(101.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=BIN2HEX(101.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2OctTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2OctTest.php index 78db6a6e..e76778f2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2OctTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Bin2OctTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Bin2OctTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerBIN2OCT * * @param mixed $expectedResult + * @param mixed $formula */ - public function testBIN2OCT($expectedResult, ...$args): void + public function testBin2Oct($expectedResult, $formula): void { - $result = Engineering::BINTOOCT(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=BIN2OCT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Bin2OctTest extends TestCase { return require 'tests/data/Calculation/Engineering/BIN2OCT.php'; } + + /** + * @dataProvider providerBIN2OCT + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testBIN2OCTOds($expectedResult, $formula): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=BIN2OCT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testBIN2OCTFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=BIN2OCT(101.1)'); + self::assertEquals(5, $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=BIN2OCT(101.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=BIN2OCT(101.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2BinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2BinTest.php index 3626ac6b..420af2c5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2BinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2BinTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Dec2BinTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerDEC2BIN * * @param mixed $expectedResult + * @param mixed $formula */ - public function testDEC2BIN($expectedResult, ...$args): void + public function testDEC2BIN($expectedResult, $formula): void { - $result = Engineering::DECTOBIN(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 5); + $sheet->getCell('A1')->setValue("=DEC2BIN($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Dec2BinTest extends TestCase { return require 'tests/data/Calculation/Engineering/DEC2BIN.php'; } + + /** + * @dataProvider providerDEC2BIN + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testDEC2BINOds($expectedResult, $formula): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 5); + $sheet->getCell('A1')->setValue("=DEC2BIN($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testDEC2BINFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=DEC2BIN(5.1)'); + self::assertEquals(101, $sheet->getCell($cell)->getCalculatedValue(), 'Gnumeric'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=DEC2BIN(5.1)'); + self::assertEquals(101, $sheet->getCell($cell)->getCalculatedValue(), 'Ods'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=DEC2BIN(5.1)'); + self::assertEquals(101, $sheet->getCell($cell)->getCalculatedValue(), 'Excel'); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2HexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2HexTest.php index d191f620..fcd9c52a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2HexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2HexTest.php @@ -3,24 +3,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Dec2HexTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerDEC2HEX * * @param mixed $expectedResult + * @param mixed $formula */ - public function testDEC2HEX($expectedResult, ...$args): void + public function testDEC2HEX($expectedResult, $formula): void { - $result = Engineering::DECTOHEX(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 17); + $sheet->getCell('A1')->setValue("=DEC2HEX($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +45,54 @@ class Dec2HexTest extends TestCase { return require 'tests/data/Calculation/Engineering/DEC2HEX.php'; } + + /** + * @dataProvider providerDEC2HEX + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testDEC2HEXOds($expectedResult, $formula): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 17); + $sheet->getCell('A1')->setValue("=DEC2HEX($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testDEC2HEXFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=DEC2HEX(17.1)'); + self::assertEquals(11, $sheet->getCell($cell)->getCalculatedValue(), 'Gnumeric'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=DEC2HEX(17.1)'); + self::assertEquals(11, $sheet->getCell($cell)->getCalculatedValue(), 'Ods'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=DEC2HEX(17.1)'); + self::assertEquals(11, $sheet->getCell($cell)->getCalculatedValue(), 'Excel'); + } + + public function test32bitHex(): void + { + self::assertEquals('A2DE246000', Engineering\ConvertDecimal::hex32bit(-400000000000, 'DE246000', true)); + self::assertEquals('7FFFFFFFFF', Engineering\ConvertDecimal::hex32bit(549755813887, 'FFFFFFFF', true)); + self::assertEquals('FFFFFFFFFF', Engineering\ConvertDecimal::hex32bit(-1, 'FFFFFFFF', true)); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2OctTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2OctTest.php index 61eb3dbb..19846a3b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2OctTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Dec2OctTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Dec2OctTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerDEC2OCT * * @param mixed $expectedResult + * @param mixed $formula */ - public function testDEC2OCT($expectedResult, ...$args): void + public function testDEC2OCT($expectedResult, $formula): void { - $result = Engineering::DECTOOCT(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 17); + $sheet->getCell('A1')->setValue("=DEC2OCT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Dec2OctTest extends TestCase { return require 'tests/data/Calculation/Engineering/DEC2OCT.php'; } + + /** + * @dataProvider providerDEC2OCT + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testDEC2OCTOds($expectedResult, $formula): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 17); + $sheet->getCell('A1')->setValue("=DEC2OCT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testDEC2OCTFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=DEC2OCT(17.1)'); + self::assertEquals(21, $sheet->getCell($cell)->getCalculatedValue(), 'Gnumeric'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=DEC2OCT(17.1)'); + self::assertEquals(21, $sheet->getCell($cell)->getCalculatedValue(), 'Ods'); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=DEC2OCT(17.1)'); + self::assertEquals(21, $sheet->getCell($cell)->getCalculatedValue(), 'Excel'); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2BinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2BinTest.php index 44d8908d..45973004 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2BinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2BinTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Hex2BinTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerHEX2BIN * * @param mixed $expectedResult + * @param mixed $formula */ - public function testHEX2BIN($expectedResult, ...$args): void + public function testHEX2BIN($expectedResult, $formula): void { - $result = Engineering::HEXTOBIN(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 'B'); + $sheet->getCell('A1')->setValue("=HEX2BIN($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,50 @@ class Hex2BinTest extends TestCase { return require 'tests/data/Calculation/Engineering/HEX2BIN.php'; } + + /** + * @dataProvider providerHEX2BIN + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testHEX2BINOds($expectedResult, $formula): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 'B'); + $sheet->getCell('A1')->setValue("=HEX2BIN($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testHEX2BINFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=HEX2BIN(10.1)'); + self::assertEquals('10000', $sheet->getCell($cell)->getCalculatedValue()); + $cell = 'F21'; + $sheet->setCellValue($cell, '=HEX2BIN("A.1")'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=HEX2BIN(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=HEX2BIN(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2DecTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2DecTest.php index b388b2b7..264ec3e3 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2DecTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2DecTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Hex2DecTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerHEX2DEC * * @param mixed $expectedResult + * @param mixed $formula */ - public function testHEX2DEC($expectedResult, ...$args): void + public function testHEX2DEC($expectedResult, $formula): void { - $result = Engineering::HEXTODEC(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 'B'); + $sheet->getCell('A1')->setValue("=HEX2DEC($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,50 @@ class Hex2DecTest extends TestCase { return require 'tests/data/Calculation/Engineering/HEX2DEC.php'; } + + /** + * @dataProvider providerHEX2DEC + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testHEX2DECOds($expectedResult, $formula): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 'B'); + $sheet->getCell('A1')->setValue("=HEX2DEC($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testHEX2DECFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=HEX2DEC(10.1)'); + self::assertEquals(16, $sheet->getCell($cell)->getCalculatedValue()); + $cell = 'F21'; + $sheet->setCellValue($cell, '=HEX2DEC("A.1")'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=HEX2DEC(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=HEX2DEC(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue(), 'Excel'); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2OctTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2OctTest.php index bc0a5cb7..15220178 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2OctTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Hex2OctTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Hex2OctTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerHEX2OCT * * @param mixed $expectedResult + * @param mixed $formula */ - public function testHEX2OCT($expectedResult, ...$args): void + public function testHEX2OCT($expectedResult, $formula): void { - $result = Engineering::HEXTOOCT(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 'B'); + $sheet->getCell('A1')->setValue("=HEX2OCT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Hex2OctTest extends TestCase { return require 'tests/data/Calculation/Engineering/HEX2OCT.php'; } + + /** + * @dataProvider providerHEX2OCT + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testHEX2OCTOds($expectedResult, $formula): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 'B'); + $sheet->getCell('A1')->setValue("=HEX2OCT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testHEX2OCTFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=HEX2OCT(10.1)'); + self::assertEquals(20, $sheet->getCell($cell)->getCalculatedValue()); + $cell = 'F21'; + $sheet->setCellValue($cell, '=HEX2OCT("A.1")'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=HEX2OCT(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=HEX2OCT(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue(), 'Excel'); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedFunctionsTest.php new file mode 100644 index 00000000..04ebb131 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedFunctionsTest.php @@ -0,0 +1,31 @@ +compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerOCT2BIN * * @param mixed $expectedResult + * @param mixed $formula */ - public function testOCT2BIN($expectedResult, ...$args): void + public function testOCT2BIN($expectedResult, $formula): void { - $result = Engineering::OCTTOBIN(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=OCT2BIN($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Oct2BinTest extends TestCase { return require 'tests/data/Calculation/Engineering/OCT2BIN.php'; } + + /** + * @dataProvider providerOCT2BIN + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testOCT2BINOds($expectedResult, $formula): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=OCT2BIN($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testOCT2BINFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=HEX2BIN(10.1)'); + self::assertEquals('10000', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=OCT2BIN(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=OCT2BIN(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2DecTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2DecTest.php index 87e213ef..b0537289 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2DecTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2DecTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Oct2DecTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerOCT2DEC * * @param mixed $expectedResult + * @param mixed $formula */ - public function testOCT2DEC($expectedResult, ...$args): void + public function testOCT2DEC($expectedResult, $formula): void { - $result = Engineering::OCTTODEC(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=OCT2DEC($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Oct2DecTest extends TestCase { return require 'tests/data/Calculation/Engineering/OCT2DEC.php'; } + + /** + * @dataProvider providerOCT2DEC + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testOCT2DECOds($expectedResult, $formula): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=OCT2DEC($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testOCT2DECFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=OCT2DEC(10.1)'); + self::assertEquals(8, $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=OCT2DEC(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=OCT2DEC(10.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2HexTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2HexTest.php index e2d75a78..05703cc9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2HexTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/Oct2HexTest.php @@ -2,25 +2,41 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class Oct2HexTest extends TestCase { + private $compatibilityMode; + protected function setUp(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->compatibilityMode = Functions::getCompatibilityMode(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); } /** * @dataProvider providerOCT2HEX * * @param mixed $expectedResult + * @param mixed $formula */ - public function testOCT2HEX($expectedResult, ...$args): void + public function testOCT2HEX($expectedResult, $formula): void { - $result = Engineering::OCTTOHEX(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=OCT2HEX($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } @@ -28,4 +44,47 @@ class Oct2HexTest extends TestCase { return require 'tests/data/Calculation/Engineering/OCT2HEX.php'; } + + /** + * @dataProvider providerOCT2HEX + * + * @param mixed $expectedResult + * @param mixed $formula + */ + public function testOCT2HEXOds($expectedResult, $formula): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + if ($formula === 'true') { + $expectedResult = 1; + } elseif ($formula === 'false') { + $expectedResult = 0; + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 101); + $sheet->getCell('A1')->setValue("=OCT2HEX($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function testOCT2HEXFrac(): void + { + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + $cell = 'G1'; + $sheet->setCellValue($cell, '=OCT2HEX(20.1)'); + self::assertEquals(10, $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + $cell = 'O1'; + $sheet->setCellValue($cell, '=OCT2HEX(20.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $cell = 'E1'; + $sheet->setCellValue($cell, '=OCT2HEX(20.1)'); + self::assertEquals('#NUM!', $sheet->getCell($cell)->getCalculatedValue()); + } } diff --git a/tests/data/Calculation/Engineering/BIN2DEC.php b/tests/data/Calculation/Engineering/BIN2DEC.php index ce9b6d34..4722d8e7 100644 --- a/tests/data/Calculation/Engineering/BIN2DEC.php +++ b/tests/data/Calculation/Engineering/BIN2DEC.php @@ -1,49 +1,26 @@ Date: Sun, 14 Mar 2021 12:05:31 -0700 Subject: [PATCH 44/89] Bitwise Functions and 32-bit (#1900) * Bitwise Functions and 32-bit When running the test suite with 32-bit PHP, a failure was reported in BITLSHIFT. In fact, all of the following are vulnerable to problems, and didn't report any failures only because of a scarcity of tests: - BITAND - BITOR - BITXOR - BITRSHIFT - BITLSHIFT Those last 2 can be resolved fairly easily by using multiplication by a power of 2 rather than shifting. The first 3 are a tougher nut to crack, and I will continue to think how they might best be approached. For now, I have added skippable tests for each of them, which at least documents the problem. Aside from adding many new tests, some bugs were correctd: - The function list in Calculation.php pointed BITXOR to BITOR. - All 5 functions allow null/false/true parameters. - BIT*SHIFT shift amount must be numeric, can be negative, allows decimal portion (which is truncated to integer), and has an absolute value limit of 53. - Because BITRSHIFT allows negative shift amount, its result can overflow (in which case return NAN). - All 5 functions disallow negative parameters (except ...SHIFT second parameter). This was coded, but the code had been thwarted by an earlier is_int test. * Full Support for AND/OR/XOR on 32-bit Previous version did not support operands 2**32 through 2**48. --- .../Calculation/Calculation.php | 2 +- .../Calculation/Engineering/BitWise.php | 103 ++++++++++++++---- .../Functions/Engineering/BitAndTest.php | 21 ++-- .../Functions/Engineering/BitLShiftTest.php | 21 ++-- .../Functions/Engineering/BitOrTest.php | 21 ++-- .../Functions/Engineering/BitRShiftTest.php | 21 ++-- .../Functions/Engineering/BitXorTest.php | 21 ++-- .../Engineering/MovedBitwiseTest.php | 24 ++++ tests/data/Calculation/Engineering/BITAND.php | 45 +++++--- .../Calculation/Engineering/BITLSHIFT.php | 46 +++++--- tests/data/Calculation/Engineering/BITOR.php | 50 +++++---- .../Calculation/Engineering/BITRSHIFT.php | 43 ++++++-- tests/data/Calculation/Engineering/BITXOR.php | 48 ++++---- 13 files changed, 308 insertions(+), 158 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedBitwiseTest.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index c88eff31..fcfebba6 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -448,7 +448,7 @@ class Calculation ], 'BITXOR' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering\BitWise::class, 'BITOR'], + 'functionCall' => [Engineering\BitWise::class, 'BITXOR'], 'argumentCount' => '2', ], 'BITLSHIFT' => [ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php b/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php index 494b5685..9958f054 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BitWise.php @@ -7,6 +7,18 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; class BitWise { + const SPLIT_DIVISOR = 2 ** 24; + + /** + * Split a number into upper and lower portions for full 32-bit support. + * + * @param float|int $number + */ + private static function splitNumber($number): array + { + return [floor($number / self::SPLIT_DIVISOR), fmod($number, self::SPLIT_DIVISOR)]; + } + /** * BITAND. * @@ -28,8 +40,10 @@ class BitWise } catch (Exception $e) { return $e->getMessage(); } + $split1 = self::splitNumber($number1); + $split2 = self::splitNumber($number2); - return $number1 & $number2; + return self::SPLIT_DIVISOR * ($split1[0] & $split2[0]) + ($split1[1] & $split2[1]); } /** @@ -54,7 +68,10 @@ class BitWise return $e->getMessage(); } - return $number1 | $number2; + $split1 = self::splitNumber($number1); + $split2 = self::splitNumber($number2); + + return self::SPLIT_DIVISOR * ($split1[0] | $split2[0]) + ($split1[1] | $split2[1]); } /** @@ -79,7 +96,10 @@ class BitWise return $e->getMessage(); } - return $number1 ^ $number2; + $split1 = self::splitNumber($number1); + $split2 = self::splitNumber($number2); + + return self::SPLIT_DIVISOR * ($split1[0] ^ $split2[0]) + ($split1[1] ^ $split2[1]); } /** @@ -93,19 +113,18 @@ class BitWise * @param int $number * @param int $shiftAmount * - * @return int|string + * @return float|int|string */ public static function BITLSHIFT($number, $shiftAmount) { try { $number = self::validateBitwiseArgument($number); + $shiftAmount = self::validateShiftAmount($shiftAmount); } catch (Exception $e) { return $e->getMessage(); } - $shiftAmount = Functions::flattenSingleValue($shiftAmount); - - $result = $number << $shiftAmount; + $result = floor($number * (2 ** $shiftAmount)); if ($result > 2 ** 48 - 1) { return Functions::NAN(); } @@ -124,19 +143,49 @@ class BitWise * @param int $number * @param int $shiftAmount * - * @return int|string + * @return float|int|string */ public static function BITRSHIFT($number, $shiftAmount) { try { $number = self::validateBitwiseArgument($number); + $shiftAmount = self::validateShiftAmount($shiftAmount); } catch (Exception $e) { return $e->getMessage(); } - $shiftAmount = Functions::flattenSingleValue($shiftAmount); + $result = floor($number / (2 ** $shiftAmount)); + if ($result > 2 ** 48 - 1) { // possible because shiftAmount can be negative + return Functions::NAN(); + } - return $number >> $shiftAmount; + return $result; + } + + /** + * Validate arguments passed to the bitwise functions. + * + * @param mixed $value + * + * @return float|int + */ + private static function validateBitwiseArgument($value) + { + self::nullFalseTrueToNumber($value); + + if (is_numeric($value)) { + if ($value == floor($value)) { + if (($value > 2 ** 48 - 1) || ($value < 0)) { + throw new Exception(Functions::NAN()); + } + + return floor($value); + } + + throw new Exception(Functions::NAN()); + } + + throw new Exception(Functions::VALUE()); } /** @@ -146,25 +195,33 @@ class BitWise * * @return int */ - private static function validateBitwiseArgument($value) + private static function validateShiftAmount($value) { - $value = Functions::flattenSingleValue($value); + self::nullFalseTrueToNumber($value); - if (is_int($value)) { - return $value; - } elseif (is_numeric($value)) { - if ($value == (int) ($value)) { - $value = (int) ($value); - if (($value > 2 ** 48 - 1) || ($value < 0)) { - throw new Exception(Functions::NAN()); - } - - return $value; + if (is_numeric($value)) { + if (abs($value) > 53) { + throw new Exception(Functions::NAN()); } - throw new Exception(Functions::NAN()); + return (int) $value; } throw new Exception(Functions::VALUE()); } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @param mixed $number + */ + public static function nullFalseTrueToNumber(&$number): void + { + $number = Functions::flattenSingleValue($number); + if ($number === null) { + $number = 0; + } elseif (is_bool($number)) { + $number = (int) $number; + } + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitAndTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitAndTest.php index e73efccc..01d006c4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitAndTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitAndTest.php @@ -2,26 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class BitAndTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerBITAND * * @param mixed $expectedResult - * @param mixed[] $args */ - public function testBITAND($expectedResult, array $args): void + public function testBITAND($expectedResult, string $formula): void { - $result = Engineering::BITAND(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 24); + $sheet->getCell('A1')->setValue("=BITAND($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitLShiftTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitLShiftTest.php index 61aa89b4..f1a716ef 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitLShiftTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitLShiftTest.php @@ -2,26 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class BitLShiftTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerBITLSHIFT * * @param mixed $expectedResult - * @param mixed[] $args */ - public function testBITLSHIFT($expectedResult, array $args): void + public function testBITLSHIFT($expectedResult, string $formula): void { - $result = Engineering::BITLSHIFT(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 8); + $sheet->getCell('A1')->setValue("=BITLSHIFT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitOrTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitOrTest.php index 857c7466..fce287d9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitOrTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitOrTest.php @@ -2,26 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class BitOrTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerBITOR * * @param mixed $expectedResult - * @param mixed[] $args */ - public function testBITOR($expectedResult, array $args): void + public function testBITOR($expectedResult, string $formula): void { - $result = Engineering::BITOR(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 8); + $sheet->getCell('A1')->setValue("=BITOR($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitRShiftTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitRShiftTest.php index 26b13d07..40b929e3 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitRShiftTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitRShiftTest.php @@ -2,26 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class BitRShiftTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerBITRSHIFT * * @param mixed $expectedResult - * @param mixed[] $args */ - public function testBITRSHIFT($expectedResult, array $args): void + public function testBITRSHIFT($expectedResult, string $formula): void { - $result = Engineering::BITRSHIFT(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 8); + $sheet->getCell('A1')->setValue("=BITRSHIFT($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitXorTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitXorTest.php index 4415f6da..847e44a2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitXorTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BitXorTest.php @@ -2,26 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Engineering; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; +use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; class BitXorTest extends TestCase { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerBITXOR * * @param mixed $expectedResult - * @param mixed[] $args */ - public function testBITXOR($expectedResult, array $args): void + public function testBITXOR($expectedResult, string $formula): void { - $result = Engineering::BITXOR(...$args); + if ($expectedResult === 'exception') { + $this->expectException(CalcExp::class); + } + $spreadsheet = new Spreadsheet(); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('A2', 8); + $sheet->getCell('A1')->setValue("=BITXOR($formula)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedBitwiseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedBitwiseTest.php new file mode 100644 index 00000000..457db0d3 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/MovedBitwiseTest.php @@ -0,0 +1,24 @@ += 2**48 + ['#NUM!', '1, power(2, 50)'], // argument >= 2**48 + ['#NUM!', '-2, 1'], // negative argument + ['#NUM!', '2, -1'], // negative argument + ['#NUM!', '-2, -1'], // negative argument + ['#NUM!', '3.1, 1'], // non-integer argument + ['#NUM!', '3, 1.1'], // non-integer argument + [0, '4, Q15'], + [0, '4, null'], + [0, '4, false'], + [1, '3, true'], + ['exception', ''], + ['exception', '2'], + [0, ', 4'], + [0, 'Q15, 4'], + [0, 'false, 4'], + [1, 'true, 5'], + [8, 'A2, 9'], ]; diff --git a/tests/data/Calculation/Engineering/BITLSHIFT.php b/tests/data/Calculation/Engineering/BITLSHIFT.php index ceaf9a33..fee922e4 100644 --- a/tests/data/Calculation/Engineering/BITLSHIFT.php +++ b/tests/data/Calculation/Engineering/BITLSHIFT.php @@ -1,20 +1,34 @@ 2**32 + [16000000000, '8000000000, 1'], // argument > 2**32 + ['#NUM!', 'power(2,50), 1'], // argument >= 2**48 + ['1', 'power(2, 47), -47'], ]; diff --git a/tests/data/Calculation/Engineering/BITOR.php b/tests/data/Calculation/Engineering/BITOR.php index 01640c9f..a98ce380 100644 --- a/tests/data/Calculation/Engineering/BITOR.php +++ b/tests/data/Calculation/Engineering/BITOR.php @@ -1,24 +1,34 @@ = 2**48 + ['#NUM!', '1, power(2, 50)'], // argument >= 2**48 + ['#NUM!', '-2, 1'], // negative argument + ['#NUM!', '2, -1'], // negative argument + ['#NUM!', '-2, -1'], // negative argument + ['#NUM!', '3.1, 1'], // non-integer argument + ['#NUM!', '3, 1.1'], // non-integer argument + [4, '4, Q15'], + [4, '4, null'], + [4, '4, false'], + [5, '4, true'], + ['exception', ''], + ['exception', '2'], + [4, ', 4'], + [4, 'Q15, 4'], + [4, 'false, 4'], + [5, 'true, 4'], + [9, 'A2, 1'], ]; diff --git a/tests/data/Calculation/Engineering/BITRSHIFT.php b/tests/data/Calculation/Engineering/BITRSHIFT.php index 78cacf37..343ccb5c 100644 --- a/tests/data/Calculation/Engineering/BITRSHIFT.php +++ b/tests/data/Calculation/Engineering/BITRSHIFT.php @@ -1,16 +1,35 @@ 2**32 + [8000000000, '16000000000, 1'], // argument > 2**32 + ['#NUM!', 'power(2,50), 1'], // argument >= 2**48 + ['1', 'power(2, 47), 47'], ]; diff --git a/tests/data/Calculation/Engineering/BITXOR.php b/tests/data/Calculation/Engineering/BITXOR.php index 40972c1c..836f327a 100644 --- a/tests/data/Calculation/Engineering/BITXOR.php +++ b/tests/data/Calculation/Engineering/BITXOR.php @@ -1,24 +1,32 @@ = 2**48 + ['#NUM!', '1, power(2, 50)'], // argument >= 2**48 + ['#NUM!', '-2, 1'], // negative argument + ['#NUM!', '2, -1'], // negative argument + ['#NUM!', '-2, -1'], // negative argument + ['#NUM!', '3.1, 1'], // non-integer argument + ['#NUM!', '3, 1.1'], // non-integer argument + [4, '4, Q15'], + [4, '4, null'], + [4, '4, false'], + [5, '4, true'], + ['exception', ''], + ['exception', '2'], + [4, ', 4'], + [4, 'Q15, 4'], + [4, 'false, 4'], + [5, 'true, 4'], + [9, 'A2, 1'], ]; From c920a776499bc20c5d30398320bf879f47e6d43d Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 14 Mar 2021 23:53:13 +0100 Subject: [PATCH 45/89] Some minor refactoring (#1923) * Some minor refactoring --- .../Calculation/Calculation.php | 99 +++++++++++-------- .../Calculation/Logical/Conditional.php | 14 +-- src/PhpSpreadsheet/Cell/Coordinate.php | 4 +- src/PhpSpreadsheet/Document/Properties.php | 30 ------ 4 files changed, 67 insertions(+), 80 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index fcfebba6..5674cb72 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3345,18 +3345,15 @@ class Calculation } /** - * @param string $cellReference * @param mixed $cellValue - * - * @return bool */ - public function getValueFromCache($cellReference, &$cellValue) + public function getValueFromCache(string $cellReference, &$cellValue): bool { + $this->debugLog->writeDebugLog("Testing cache value for cell {$cellReference}"); // Is calculation cacheing enabled? - // Is the value present in calculation cache? - $this->debugLog->writeDebugLog('Testing cache value for cell ', $cellReference); + // 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 {$cellReference} from cache"); // Return the cached result $cellValue = $this->calculationCache[$cellReference]; @@ -3418,7 +3415,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 {$wsCellReference}"); if (($wsTitle[0] !== "\x00") && ($this->cyclicReferenceStack->onStack($wsCellReference))) { if ($this->cyclicFormulaCount <= 0) { @@ -3440,7 +3437,7 @@ class Calculation } } - $this->debugLog->writeDebugLog('Formula for cell ', $wsCellReference, ' is ', $formula); + $this->debugLog->writeDebugLog("Formula for cell {$wsCellReference} is {$formula}"); // Parse the formula onto the token stack and calculate the value $this->cyclicReferenceStack->push($wsCellReference); @@ -4805,6 +4802,53 @@ class Calculation return true; } + /** + * @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) + { + $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); + $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); + $r = $stack->pop(); + $result[$x] = $r['value']; + } + } else { + // Operand 1 and Operand 2 are both arrays + if (!$recursingArrays) { + 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); + $r = $stack->pop(); + $result[$x] = $r['value']; + } + } + // Log the result details + $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result)); + // And push the result onto the stack + $stack->push('Array', $result); + + return $result; + } + /** * @param null|string $cellID * @param mixed $operand1 @@ -4818,38 +4862,7 @@ class Calculation { // If we're dealing with matrix operations, we want a matrix result if ((is_array($operand1)) || (is_array($operand2))) { - $result = []; - if ((is_array($operand1)) && (!is_array($operand2))) { - foreach ($operand1 as $x => $operandData) { - $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operandData), ' ', $operation, ' ', $this->showValue($operand2)); - $this->executeBinaryComparisonOperation($cellID, $operandData, $operand2, $operation, $stack); - $r = $stack->pop(); - $result[$x] = $r['value']; - } - } elseif ((!is_array($operand1)) && (is_array($operand2))) { - foreach ($operand2 as $x => $operandData) { - $this->debugLog->writeDebugLog('Evaluating Comparison ', $this->showValue($operand1), ' ', $operation, ' ', $this->showValue($operandData)); - $this->executeBinaryComparisonOperation($cellID, $operand1, $operandData, $operation, $stack); - $r = $stack->pop(); - $result[$x] = $r['value']; - } - } else { - if (!$recursingArrays) { - 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); - $r = $stack->pop(); - $result[$x] = $r['value']; - } - } - // Log the result details - $this->debugLog->writeDebugLog('Comparison Evaluation Result is ', $this->showTypeDetails($result)); - // And push the result onto the stack - $stack->push('Array', $result); - - return $result; + return $this->executeArrayComparison($cellID, $operand1, $operand2, $operation, $stack, $recursingArrays); } // Simple validate the two operands if they are string values @@ -4863,10 +4876,10 @@ class Calculation // Use case insensitive comparaison if not OpenOffice mode if (Functions::getCompatibilityMode() != Functions::COMPATIBILITY_OPENOFFICE) { if (is_string($operand1)) { - $operand1 = strtoupper($operand1); + $operand1 = Shared\StringHelper::strToUpper($operand1); } if (is_string($operand2)) { - $operand2 = strtoupper($operand2); + $operand2 = Shared\StringHelper::strToUpper($operand2); } } diff --git a/src/PhpSpreadsheet/Calculation/Logical/Conditional.php b/src/PhpSpreadsheet/Calculation/Logical/Conditional.php index 12256d34..e84d0f33 100644 --- a/src/PhpSpreadsheet/Calculation/Logical/Conditional.php +++ b/src/PhpSpreadsheet/Calculation/Logical/Conditional.php @@ -83,11 +83,11 @@ class Conditional $targetValue = Functions::flattenSingleValue($arguments[0]); $argc = count($arguments) - 1; $switchCount = floor($argc / 2); - $switchSatisfied = false; $hasDefaultClause = $argc % 2 !== 0; - $defaultClause = $argc % 2 === 0 ? null : $arguments[count($arguments) - 1]; + $defaultClause = $argc % 2 === 0 ? null : $arguments[$argc]; - if ($switchCount) { + $switchSatisfied = false; + if ($switchCount > 0) { for ($index = 0; $index < $switchCount; ++$index) { if ($targetValue == $arguments[$index * 2 + 1]) { $result = $arguments[$index * 2 + 2]; @@ -98,7 +98,7 @@ class Conditional } } - if (!$switchSatisfied) { + if ($switchSatisfied !== true) { $result = $hasDefaultClause ? $defaultClause : Functions::NA(); } } @@ -161,12 +161,14 @@ class Conditional */ public static function IFS(...$arguments) { - if (count($arguments) % 2 != 0) { + $argumentCount = count($arguments); + + if ($argumentCount % 2 != 0) { return Functions::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 < count($arguments); $i += 2) { + 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]); $result = self::statementIf($testValue, $returnIfTrue, $falseValueException); diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 2afeebe9..8d81f3a1 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -339,7 +339,8 @@ abstract class Coordinate private static function processRangeSetOperators(array $operators, array $cells): array { - for ($offset = 0; $offset < count($operators); ++$offset) { + $operatorCount = count($operators); + for ($offset = 0; $offset < $operatorCount; ++$offset) { $operator = $operators[$offset]; if ($operator !== ' ') { continue; @@ -350,6 +351,7 @@ abstract class Coordinate $operators = array_values($operators); $cells = array_values($cells); --$offset; + --$operatorCount; } return $cells; diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 0876a9ed..d6aff81e 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -506,49 +506,33 @@ class Properties switch ($propertyType) { case 'empty': // Empty return ''; - - break; case 'null': // Null return null; - - break; case 'i1': // 1-Byte Signed Integer case 'i2': // 2-Byte Signed Integer case 'i4': // 4-Byte Signed Integer case 'i8': // 8-Byte Signed Integer case 'int': // Integer return (int) $propertyValue; - - break; case 'ui1': // 1-Byte Unsigned Integer case 'ui2': // 2-Byte Unsigned Integer case 'ui4': // 4-Byte Unsigned Integer case 'ui8': // 8-Byte Unsigned Integer case 'uint': // Unsigned Integer return abs((int) $propertyValue); - - break; case 'r4': // 4-Byte Real Number case 'r8': // 8-Byte Real Number case 'decimal': // Decimal return (float) $propertyValue; - - break; case 'lpstr': // LPSTR case 'lpwstr': // LPWSTR case 'bstr': // Basic String return $propertyValue; - - break; case 'date': // Date and Time case 'filetime': // File Time return strtotime($propertyValue); - - break; case 'bool': // Boolean return $propertyValue == 'true'; - - break; case 'cy': // Currency case 'error': // Error Status Code case 'vector': // Vector @@ -563,8 +547,6 @@ class Properties case 'clsid': // Class ID case 'cf': // Clipboard Data return $propertyValue; - - break; } return $propertyValue; @@ -584,31 +566,21 @@ class Properties case 'ui8': // 8-Byte Unsigned Integer case 'uint': // Unsigned Integer return self::PROPERTY_TYPE_INTEGER; - - break; case 'r4': // 4-Byte Real Number case 'r8': // 8-Byte Real Number case 'decimal': // Decimal return self::PROPERTY_TYPE_FLOAT; - - break; case 'empty': // Empty case 'null': // Null case 'lpstr': // LPSTR case 'lpwstr': // LPWSTR case 'bstr': // Basic String return self::PROPERTY_TYPE_STRING; - - break; case 'date': // Date and Time case 'filetime': // File Time return self::PROPERTY_TYPE_DATE; - - break; case 'bool': // Boolean return self::PROPERTY_TYPE_BOOLEAN; - - break; case 'cy': // Currency case 'error': // Error Status Code case 'vector': // Vector @@ -623,8 +595,6 @@ class Properties case 'clsid': // Class ID case 'cf': // Clipboard Data return self::PROPERTY_TYPE_UNKNOWN; - - break; } return self::PROPERTY_TYPE_UNKNOWN; From ae2468426fd77769cfdd0ea01595d6c28e911425 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 15 Mar 2021 14:14:44 +0100 Subject: [PATCH 46/89] jpgraph seems to be finally dying with PHP. (#1926) * jpgraph seems to be finally dying with PHP. Until we have a valid alternative, disabling this run for PHP because it errors https://github.com/HuasoFoundries/jpgraph looks like a natural successor, but it isn't BC so it will require some work to integrate --- samples/Chart/35_Chart_render.php | 5 +++++ tests/PhpSpreadsheetTests/Helper/SampleTest.php | 1 + 2 files changed, 6 insertions(+) diff --git a/samples/Chart/35_Chart_render.php b/samples/Chart/35_Chart_render.php index 9638c679..ebab16a7 100644 --- a/samples/Chart/35_Chart_render.php +++ b/samples/Chart/35_Chart_render.php @@ -5,6 +5,11 @@ use PhpOffice\PhpSpreadsheet\Settings; require __DIR__ . '/../Header.php'; +if (PHP_VERSION_ID >= 80000) { + $helper->log('Jpgraph no longer runs against PHP8'); + exit; +} + // Change these values to select the Rendering library that you wish to use Settings::setChartRenderer(\PhpOffice\PhpSpreadsheet\Chart\Renderer\JpGraph::class); diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index cd87eda2..a604bcfc 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -30,6 +30,7 @@ class SampleTest extends TestCase $skipped = [ 'Chart/32_Chart_read_write_PDF.php', // Unfortunately JpGraph is not up to date for latest PHP and raise many warnings 'Chart/32_Chart_read_write_HTML.php', // idem + 'Chart/35_Chart_render.php', // idem ]; // TCPDF and DomPDF libraries don't support PHP8 yet if (\PHP_VERSION_ID >= 80000) { From 09022256f4b6095c51c77e1187b0a2d32efbca0d Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 15 Mar 2021 14:50:05 +0100 Subject: [PATCH 47/89] Resolve Deprecated setMethods() call when Mocking for tests (#1925) Resolve Deprecated `setMethods()` calls when Mocking for tests, using `onlyMethods()` and `addMethods()` instead --- .../Functions/LookupRef/ColumnTest.php | 2 +- .../Functions/LookupRef/RowTest.php | 2 +- .../Functions/MathTrig/SubTotalTest.php | 22 +++++++++---------- .../Cell/AdvancedValueBinderTest.php | 15 ++++++++----- .../Collection/CellsTest.php | 2 +- .../PhpSpreadsheetTests/Helper/SampleTest.php | 2 +- 6 files changed, 25 insertions(+), 20 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php index 363c6c1b..61c7d40d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/ColumnTest.php @@ -34,7 +34,7 @@ class ColumnTest extends TestCase public function testCOLUMNwithNull(): void { $cell = $this->getMockBuilder(Cell::class) - ->setMethods(['getColumn']) + ->onlyMethods(['getColumn']) ->disableOriginalConstructor() ->getMock(); $cell->method('getColumn') diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php index 804b924d..29a72a28 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/RowTest.php @@ -34,7 +34,7 @@ class RowTest extends TestCase public function testROWwithNull(): void { $cell = $this->getMockBuilder(Cell::class) - ->setMethods(['getRow']) + ->onlyMethods(['getRow']) ->disableOriginalConstructor() ->getMock(); $cell->method('getRow') diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php index efee60bd..a629a7f4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php @@ -25,7 +25,7 @@ class SubTotalTest extends TestCase public function testSUBTOTAL($expectedResult, ...$args): void { $cell = $this->getMockBuilder(Cell::class) - ->setMethods(['getValue', 'isFormula']) + ->onlyMethods(['getValue', 'isFormula']) ->disableOriginalConstructor() ->getMock(); $cell->method('getValue') @@ -33,7 +33,7 @@ class SubTotalTest extends TestCase $cell->method('getValue') ->willReturn(false); $worksheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['cellExists', 'getCell']) + ->onlyMethods(['cellExists', 'getCell']) ->disableOriginalConstructor() ->getMock(); $worksheet->method('cellExists') @@ -41,7 +41,7 @@ class SubTotalTest extends TestCase $worksheet->method('getCell') ->willReturn($cell); $cellReference = $this->getMockBuilder(Cell::class) - ->setMethods(['getWorksheet']) + ->onlyMethods(['getWorksheet']) ->disableOriginalConstructor() ->getMock(); $cellReference->method('getWorksheet') @@ -75,7 +75,7 @@ class SubTotalTest extends TestCase $visibilityGenerator = $this->rowVisibility($hiddenRows); $rowDimension = $this->getMockBuilder(RowDimension::class) - ->setMethods(['getVisible']) + ->onlyMethods(['getVisible']) ->disableOriginalConstructor() ->getMock(); $rowDimension->method('getVisible') @@ -86,13 +86,13 @@ class SubTotalTest extends TestCase return $result; }); $columnDimension = $this->getMockBuilder(ColumnDimension::class) - ->setMethods(['getVisible']) + ->onlyMethods(['getVisible']) ->disableOriginalConstructor() ->getMock(); $columnDimension->method('getVisible') ->willReturn(true); $cell = $this->getMockBuilder(Cell::class) - ->setMethods(['getValue', 'isFormula']) + ->onlyMethods(['getValue', 'isFormula']) ->disableOriginalConstructor() ->getMock(); $cell->method('getValue') @@ -100,7 +100,7 @@ class SubTotalTest extends TestCase $cell->method('getValue') ->willReturn(false); $worksheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['cellExists', 'getCell', 'getRowDimension', 'getColumnDimension']) + ->onlyMethods(['cellExists', 'getCell', 'getRowDimension', 'getColumnDimension']) ->disableOriginalConstructor() ->getMock(); $worksheet->method('cellExists') @@ -112,7 +112,7 @@ class SubTotalTest extends TestCase $worksheet->method('getColumnDimension') ->willReturn($columnDimension); $cellReference = $this->getMockBuilder(Cell::class) - ->setMethods(['getWorksheet']) + ->onlyMethods(['getWorksheet']) ->disableOriginalConstructor() ->getMock(); $cellReference->method('getWorksheet') @@ -153,7 +153,7 @@ class SubTotalTest extends TestCase $cellIsFormulaGenerator = $this->cellIsFormula(Functions::flattenArray(array_slice($args, 1))); $cell = $this->getMockBuilder(Cell::class) - ->setMethods(['getValue', 'isFormula']) + ->onlyMethods(['getValue', 'isFormula']) ->disableOriginalConstructor() ->getMock(); $cell->method('getValue') @@ -171,7 +171,7 @@ class SubTotalTest extends TestCase return $result; }); $worksheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['cellExists', 'getCell']) + ->onlyMethods(['cellExists', 'getCell']) ->disableOriginalConstructor() ->getMock(); $worksheet->method('cellExists') @@ -179,7 +179,7 @@ class SubTotalTest extends TestCase $worksheet->method('getCell') ->willReturn($cell); $cellReference = $this->getMockBuilder(Cell::class) - ->setMethods(['getWorksheet']) + ->onlyMethods(['getWorksheet']) ->disableOriginalConstructor() ->getMock(); $cellReference->method('getWorksheet') diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index eefa7b83..e71e3ad2 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -46,7 +46,8 @@ class AdvancedValueBinderTest extends TestCase public function testCurrency($value, $valueBinded, $format, $thousandsSeparator, $decimalSeparator, $currencyCode): void { $sheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['getStyle', 'getNumberFormat', 'setFormatCode', 'getCellCollection']) + ->onlyMethods(['getStyle', 'getCellCollection']) + ->addMethods(['getNumberFormat', 'setFormatCode']) ->getMock(); $cellCollection = $this->getMockBuilder(Cells::class) ->disableOriginalConstructor() @@ -106,7 +107,8 @@ class AdvancedValueBinderTest extends TestCase public function testFractions($value, $valueBinded, $format): void { $sheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['getStyle', 'getNumberFormat', 'setFormatCode', 'getCellCollection']) + ->onlyMethods(['getStyle', 'getCellCollection']) + ->addMethods(['getNumberFormat', 'setFormatCode']) ->getMock(); $cellCollection = $this->getMockBuilder(Cells::class) @@ -164,7 +166,8 @@ class AdvancedValueBinderTest extends TestCase public function testPercentages($value, $valueBinded, $format): void { $sheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['getStyle', 'getNumberFormat', 'setFormatCode', 'getCellCollection']) + ->onlyMethods(['getStyle', 'getCellCollection']) + ->addMethods(['getNumberFormat', 'setFormatCode']) ->getMock(); $cellCollection = $this->getMockBuilder(Cells::class) ->disableOriginalConstructor() @@ -214,7 +217,8 @@ class AdvancedValueBinderTest extends TestCase public function testTimes($value, $valueBinded, $format): void { $sheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['getStyle', 'getNumberFormat', 'setFormatCode', 'getCellCollection']) + ->onlyMethods(['getStyle', 'getCellCollection']) + ->addMethods(['getNumberFormat', 'setFormatCode']) ->getMock(); $cellCollection = $this->getMockBuilder(Cells::class) @@ -265,7 +269,8 @@ class AdvancedValueBinderTest extends TestCase public function testStringWrapping(string $value, bool $wrapped): void { $sheet = $this->getMockBuilder(Worksheet::class) - ->setMethods(['getStyle', 'getAlignment', 'setWrapText', 'getCellCollection']) + ->onlyMethods(['getStyle', 'getCellCollection']) + ->addMethods(['getAlignment', 'setWrapText']) ->getMock(); $cellCollection = $this->getMockBuilder(Cells::class) ->disableOriginalConstructor() diff --git a/tests/PhpSpreadsheetTests/Collection/CellsTest.php b/tests/PhpSpreadsheetTests/Collection/CellsTest.php index 539d0232..5e656cf5 100644 --- a/tests/PhpSpreadsheetTests/Collection/CellsTest.php +++ b/tests/PhpSpreadsheetTests/Collection/CellsTest.php @@ -91,7 +91,7 @@ class CellsTest extends TestCase $collection = $this->getMockBuilder(Cells::class) ->setConstructorArgs([new Worksheet(), new Memory()]) - ->setMethods(['has']) + ->onlyMethods(['has']) ->getMock(); $collection->method('has') diff --git a/tests/PhpSpreadsheetTests/Helper/SampleTest.php b/tests/PhpSpreadsheetTests/Helper/SampleTest.php index a604bcfc..8956771c 100644 --- a/tests/PhpSpreadsheetTests/Helper/SampleTest.php +++ b/tests/PhpSpreadsheetTests/Helper/SampleTest.php @@ -30,7 +30,6 @@ class SampleTest extends TestCase $skipped = [ 'Chart/32_Chart_read_write_PDF.php', // Unfortunately JpGraph is not up to date for latest PHP and raise many warnings 'Chart/32_Chart_read_write_HTML.php', // idem - 'Chart/35_Chart_render.php', // idem ]; // TCPDF and DomPDF libraries don't support PHP8 yet if (\PHP_VERSION_ID >= 80000) { @@ -39,6 +38,7 @@ class SampleTest extends TestCase [ 'Pdf/21_Pdf_Domdf.php', 'Pdf/21_Pdf_TCPDF.php', + 'Chart/35_Chart_render.php', // idem ] ); } From 9b67e3f5979835ad2da4f6c6c0cb1d84ad58d18e Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 15 Mar 2021 23:02:41 +0100 Subject: [PATCH 48/89] Fix error with a single byte being removed after the _ spacing character when rendering number formats (#1927) * Fix error with a single byte being removed after the _ spacing character when rendering number formats --- CHANGELOG.md | 1 + src/PhpSpreadsheet/Style/NumberFormat.php | 4 ++-- tests/data/Style/NumberFormat.php | 9 +++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2565a768..86be590b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fixed issue with _ spacing character in number format mask corrumpting output from toFormattedString() [Issue 1924#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1924) [PR #1927](https://github.com/PHPOffice/PhpSpreadsheet/pull/1927) - Fix for [Issue #1887](https://github.com/PHPOffice/PhpSpreadsheet/issues/1887) - Lose Track of Selected Cells After Save - Fixed issue with Xlsx@listWorksheetInfo not returning any data - Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions. [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640) diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index a623657b..3c6985a2 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -848,7 +848,7 @@ class NumberFormat extends Supervisor ); // Convert any other escaped characters to quoted strings, e.g. (\T to "T") - $format = preg_replace('/(\\\(((.)(?!((AM\/PM)|(A\/P))))|([^ ])))(?=(?:[^"]|"[^"]*")*$)/u', '"${2}"', $format); + $format = 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); @@ -857,7 +857,7 @@ class NumberFormat extends Supervisor // 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('/_./', ' ', $format); + $format = preg_replace('/_(.)/ui', ' ${1}', $format); // Let's begin inspecting the format and converting the value to a formatted string diff --git a/tests/data/Style/NumberFormat.php b/tests/data/Style/NumberFormat.php index 81bb90ae..db14b0e1 100644 --- a/tests/data/Style/NumberFormat.php +++ b/tests/data/Style/NumberFormat.php @@ -128,6 +128,11 @@ return [ 12345.678900000001, '#,##0.000\ [$]', ], + 'Spacing Character' => [ + '826.00 €', + 826, + '#,##0.00 _€', + ], [ '5.68', 5.6788999999999996, @@ -294,12 +299,12 @@ return [ '[$-1010409]#,##0.00;-#,##0.00', ], [ - ' $ 23.06 ', + ' ($ 23.06 )', 23.0597, '_("$"* #,##0.00_);_("$"* \(#,##0.00\);_("$"* "-"??_);_(@_)', ], [ - ' € 13.03 ', + ' (€ 13.03 )', 13.0316, '_("€"* #,##0.00_);_("€"* \(#,##0.00\);_("€"* "-"??_);_(@_)', ], From 6490c3ff0ae10b0e2e7649c119c186dcb3df28bf Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 17 Mar 2021 12:18:34 +0100 Subject: [PATCH 49/89] First step in some refactoring of the NumberFormat class (#1928) * Refactoring of the NumberFormat class; separate the cell numberformat properties from the actually code used to format a value, leaving just a callthrough stub * Resolve issue with percentage formatter, and provide support for ? placeholders in percentage formatting --- src/PhpSpreadsheet/Style/NumberFormat.php | 490 +----------------- .../Style/NumberFormat/BaseFormatter.php | 12 + .../Style/NumberFormat/DateFormatter.php | 129 +++++ .../Style/NumberFormat/Formatter.php | 162 ++++++ .../Style/NumberFormat/FractionFormatter.php | 45 ++ .../Style/NumberFormat/NumberFormatter.php | 181 +++++++ .../NumberFormat/PercentageFormatter.php | 42 ++ tests/data/Style/NumberFormat.php | 75 +++ 8 files changed, 647 insertions(+), 489 deletions(-) create mode 100644 src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php create mode 100644 src/PhpSpreadsheet/Style/NumberFormat/DateFormatter.php create mode 100644 src/PhpSpreadsheet/Style/NumberFormat/Formatter.php create mode 100644 src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php create mode 100644 src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php create mode 100644 src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php diff --git a/src/PhpSpreadsheet/Style/NumberFormat.php b/src/PhpSpreadsheet/Style/NumberFormat.php index 3c6985a2..b5cc05f4 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat.php +++ b/src/PhpSpreadsheet/Style/NumberFormat.php @@ -2,10 +2,6 @@ namespace PhpOffice\PhpSpreadsheet\Style; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; - class NumberFormat extends Supervisor { // Pre-defined formats @@ -389,434 +385,6 @@ class NumberFormat extends Supervisor ); } - /** - * Search/replace values to convert Excel date/time format masks to PHP format masks. - * - * @var array - */ - private static $dateFormatReplacements = [ - // first remove escapes related to non-format characters - '\\' => '', - // 12-hour suffix - 'am/pm' => 'A', - // 4-digit year - 'e' => 'Y', - 'yyyy' => 'Y', - // 2-digit year - 'yy' => 'y', - // first letter of month - no php equivalent - 'mmmmm' => 'M', - // full month name - 'mmmm' => 'F', - // short month name - 'mmm' => 'M', - // mm is minutes if time, but can also be month w/leading zero - // so we try to identify times be the inclusion of a : separator in the mask - // 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 - 'ddd' => 'D', - // days leading zero - '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 = [ - 'hh' => 'H', - 'h' => 'G', - ]; - - /** - * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock). - * - * @var array - */ - private static $dateFormatReplacements12 = [ - 'hh' => 'h', - 'h' => 'g', - ]; - - private static function setLowercaseCallback($matches) - { - return mb_strtolower($matches[0]); - } - - private static function escapeQuotesCallback($matches) - { - return '\\' . implode('\\', str_split($matches[1])); - } - - private static function formatAsDate(&$value, &$format): void - { - // 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); - - // 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); - - // 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); - 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); - } else { - // 12-hour time format - $block = strtr($block, self::$dateFormatReplacements12); - } - } - } - $format = implode('"', $blocks); - - // escape any quoted characters so that DateTime format() will render them correctly - $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $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); - $value = $dateObj->format($format); - } - - private static function formatAsPercentage(&$value, &$format): void - { - if ($format === self::FORMAT_PERCENTAGE) { - $value = round((100 * $value), 0) . '%'; - } else { - if (preg_match('/\.[#0]+/', $format, $m)) { - $s = substr($m[0], 0, 1) . (strlen($m[0]) - 1); - $format = str_replace($m[0], $s, $format); - } - if (preg_match('/^[#0]+/', $format, $m)) { - $format = str_replace($m[0], strlen($m[0]), $format); - } - $format = '%' . str_replace('%', 'f%%', $format); - - $value = sprintf($format, 100 * $value); - } - } - - private static function formatAsFraction(&$value, &$format): void - { - $sign = ($value < 0) ? '-' : ''; - - $integerPart = floor(abs($value)); - $decimalPart = trim(fmod(abs($value), 1), '0.'); - $decimalLength = strlen($decimalPart); - $decimalDivisor = 10 ** $decimalLength; - - $GCD = MathTrig::GCD($decimalPart, $decimalDivisor); - - $adjustedDecimalPart = $decimalPart / $GCD; - $adjustedDecimalDivisor = $decimalDivisor / $GCD; - - if ((strpos($format, '0') !== false)) { - $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor"; - } elseif ((strpos($format, '#') !== false)) { - if ($integerPart == 0) { - $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor"; - } else { - $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor"; - } - } elseif ((substr($format, 0, 3) == '? ?')) { - if ($integerPart == 0) { - $integerPart = ''; - } - $value = "$sign$integerPart $adjustedDecimalPart/$adjustedDecimalDivisor"; - } else { - $adjustedDecimalPart += $integerPart * $adjustedDecimalDivisor; - $value = "$sign$adjustedDecimalPart/$adjustedDecimalDivisor"; - } - } - - private static function mergeComplexNumberFormatMasks($numbers, $masks) - { - $decimalCount = strlen($numbers[1]); - $postDecimalMasks = []; - - do { - $tempMask = array_pop($masks); - if ($tempMask !== null) { - $postDecimalMasks[] = $tempMask; - $decimalCount -= strlen($tempMask); - } - } while ($tempMask !== null && $decimalCount > 0); - - return [ - implode('.', $masks), - implode('.', array_reverse($postDecimalMasks)), - ]; - } - - private static function processComplexNumberFormatMask($number, $mask) - { - $result = $number; - $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE); - - if ($maskingBlockCount > 1) { - $maskingBlocks = array_reverse($maskingBlocks[0]); - - foreach ($maskingBlocks as $block) { - $divisor = 1 . $block[0]; - $size = strlen($block[0]); - $offset = $block[1]; - - $blockValue = sprintf( - '%0' . $size . 'd', - fmod($number, $divisor) - ); - $number = floor($number / $divisor); - $mask = substr_replace($mask, $blockValue, $offset, $size); - } - if ($number > 0) { - $mask = substr_replace($mask, $number, $offset, 0); - } - $result = $mask; - } - - return $result; - } - - private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true) - { - $sign = ($number < 0.0); - $number = abs($number); - - if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) { - $numbers = explode('.', $number); - $masks = explode('.', $mask); - if (count($masks) > 2) { - $masks = self::mergeComplexNumberFormatMasks($numbers, $masks); - } - $result1 = self::complexNumberFormatMask($numbers[0], $masks[0], false); - $result2 = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false)); - - return (($sign) ? '-' : '') . $result1 . '.' . $result2; - } - - $result = self::processComplexNumberFormatMask($number, $mask); - - return (($sign) ? '-' : '') . $result; - } - - private static function formatStraightNumericValue($value, $format, array $matches, $useThousands, $number_regex) - { - $left = $matches[1]; - $dec = $matches[2]; - $right = $matches[3]; - - // minimun width of formatted number (including dot) - $minWidth = strlen($left) + strlen($dec) + strlen($right); - if ($useThousands) { - $value = number_format( - $value, - strlen($right), - StringHelper::getDecimalSeparator(), - StringHelper::getThousandsSeparator() - ); - $value = preg_replace($number_regex, $value, $format); - } else { - if (preg_match('/[0#]E[+-]0/i', $format)) { - // Scientific format - $value = sprintf('%5.2E', $value); - } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) { - if ($value == (int) $value && substr_count($format, '.') === 1) { - $value *= 10 ** strlen(explode('.', $format)[1]); - } - $value = self::complexNumberFormatMask($value, $format); - } else { - $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; - $value = sprintf($sprintf_pattern, $value); - $value = preg_replace($number_regex, $value, $format); - } - } - - return $value; - } - - private static function formatAsNumber($value, $format) - { - // The "_" in this string has already been stripped out, - // so this test is never true. Furthermore, testing - // on Excel shows this format uses Euro symbol, not "EUR". - //if ($format === self::FORMAT_CURRENCY_EUR_SIMPLE) { - // return 'EUR ' . sprintf('%1.2f', $value); - //} - - // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols - $format = str_replace(['"', '*'], '', $format); - - // Find out if we need thousands separator - // This is indicated by a comma enclosed by a digit placeholder: - // #,# or 0,0 - $useThousands = preg_match('/(#,#|0,0)/', $format); - if ($useThousands) { - $format = preg_replace('/0,0/', '00', $format); - $format = preg_replace('/#,#/', '##', $format); - } - - // Scale thousands, millions,... - // This is indicated by a number of commas after a digit placeholder: - // #, or 0.0,, - $scale = 1; // same as no scale - $matches = []; - if (preg_match('/(#|0)(,+)/', $format, $matches)) { - $scale = 1000 ** strlen($matches[2]); - - // strip the commas - $format = preg_replace('/0,+/', '0', $format); - $format = preg_replace('/#,+/', '#', $format); - } - - if (preg_match('/#?.*\?\/\?/', $format, $m)) { - if ($value != (int) $value) { - self::formatAsFraction($value, $format); - } - } else { - // Handle the number itself - - // scale number - $value = $value / $scale; - // Strip # - $format = preg_replace('/\\#/', '0', $format); - // Remove locale code [$-###] - $format = preg_replace('/\[\$\-.*\]/', '', $format); - - $n = '/\\[[^\\]]+\\]/'; - $m = preg_replace($n, '', $format); - $number_regex = '/(0+)(\\.?)(0*)/'; - if (preg_match($number_regex, $m, $matches)) { - $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands, $number_regex); - } - } - - if (preg_match('/\[\$(.*)\]/u', $format, $m)) { - // Currency or Accounting - $currencyCode = $m[1]; - [$currencyCode] = explode('-', $currencyCode); - if ($currencyCode == '') { - $currencyCode = StringHelper::getCurrencyCode(); - } - $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value); - } - - return $value; - } - - private static function splitFormatCompare($value, $cond, $val, $dfcond, $dfval) - { - if (!$cond) { - $cond = $dfcond; - $val = $dfval; - } - switch ($cond) { - case '>': - return $value > $val; - - case '<': - return $value < $val; - - case '<=': - return $value <= $val; - - case '<>': - return $value != $val; - - case '=': - return $value == $val; - } - - return $value >= $val; - } - - private static function splitFormat($sections, $value) - { - // Extract the relevant section depending on whether number is positive, negative, or zero? - // Text not supported yet. - // Here is how the sections apply to various values in Excel: - // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT] - // 2 sections: [POSITIVE/ZERO/TEXT] [NEGATIVE] - // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO] - // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT] - $cnt = count($sections); - $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . ')\\]/'; - $cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/'; - $colors = ['', '', '', '', '']; - $condops = ['', '', '', '', '']; - $condvals = [0, 0, 0, 0, 0]; - 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]); - } - if (preg_match($cond_regex, $sections[$idx], $matches)) { - $condops[$idx] = $matches[1]; - $condvals[$idx] = $matches[2]; - $sections[$idx] = preg_replace($cond_regex, '', $sections[$idx]); - } - } - $color = $colors[0]; - $format = $sections[0]; - $absval = $value; - switch ($cnt) { - case 2: - $absval = abs($value); - if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>=', 0)) { - $color = $colors[1]; - $format = $sections[1]; - } - - break; - case 3: - case 4: - $absval = abs($value); - if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>', 0)) { - if (self::splitFormatCompare($value, $condops[1], $condvals[1], '<', 0)) { - $color = $colors[1]; - $format = $sections[1]; - } else { - $color = $colors[2]; - $format = $sections[2]; - } - } - - break; - } - - return [$color, $format, $absval]; - } - /** * Convert a value in a pre-defined format to a PHP string. * @@ -828,63 +396,7 @@ class NumberFormat extends Supervisor */ public static function toFormattedString($value, $format, $callBack = null) { - // For now we do not treat strings although section 4 of a format code affects strings - if (!is_numeric($value)) { - return $value; - } - - // For 'General' format code, we just pass the value although this is not entirely the way Excel does it, - // it seems to round numbers to a total of 10 digits. - if (($format === self::FORMAT_GENERAL) || ($format === self::FORMAT_TEXT)) { - return $value; - } - - $format = preg_replace_callback( - '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', - function ($matches) { - return str_replace('.', chr(0x00), $matches[0]); - }, - $format - ); - - // Convert any other escaped characters to quoted strings, e.g. (\T to "T") - $format = 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); - - [$colors, $format, $value] = self::splitFormat($sections, $value); - - // 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', ' ${1}', $format); - - // Let's begin inspecting the format and converting the value to a formatted string - - // Check for date/time characters (not inside quotes) - if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) { - // datetime format - self::formatAsDate($value, $format); - } else { - if (substr($format, 0, 1) === '"' && substr($format, -1, 1) === '"') { - $value = substr($format, 1, -1); - } elseif (preg_match('/%$/', $format)) { - // % number format - self::formatAsPercentage($value, $format); - } else { - $value = self::formatAsNumber($value, $format); - } - } - - // Additional formatting provided by callback function - if ($callBack !== null) { - [$writerInstance, $function] = $callBack; - $value = $writerInstance->$function($value, $colors); - } - - $value = str_replace(chr(0x00), '.', $value); - - return $value; + return NumberFormat\Formatter::toFormattedString($value, $format, $callBack); } protected function exportArray1(): array diff --git a/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php new file mode 100644 index 00000000..7988143c --- /dev/null +++ b/src/PhpSpreadsheet/Style/NumberFormat/BaseFormatter.php @@ -0,0 +1,12 @@ + '', + // 12-hour suffix + 'am/pm' => 'A', + // 4-digit year + 'e' => 'Y', + 'yyyy' => 'Y', + // 2-digit year + 'yy' => 'y', + // first letter of month - no php equivalent + 'mmmmm' => 'M', + // full month name + 'mmmm' => 'F', + // short month name + 'mmm' => 'M', + // mm is minutes if time, but can also be month w/leading zero + // so we try to identify times be the inclusion of a : separator in the mask + // 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 + 'ddd' => 'D', + // days leading zero + '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 = [ + 'hh' => 'H', + 'h' => 'G', + ]; + + /** + * Search/replace values to convert Excel date/time format masks hours to PHP format masks (12 hr clock). + * + * @var array + */ + private static $dateFormatReplacements12 = [ + 'hh' => 'h', + 'h' => 'g', + ]; + + 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); + + // 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); + + // 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); + 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); + } else { + // 12-hour time format + $block = strtr($block, self::$dateFormatReplacements12); + } + } + } + $format = implode('"', $blocks); + + // escape any quoted characters so that DateTime format() will render them correctly + $format = preg_replace_callback('/"(.*)"/U', ['self', 'escapeQuotesCallback'], $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); + + return $dateObj->format($format); + } + + private static function setLowercaseCallback($matches): string + { + return mb_strtolower($matches[0]); + } + + private static function escapeQuotesCallback($matches): string + { + return '\\' . implode('\\', str_split($matches[1])); + } +} diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php new file mode 100644 index 00000000..6fa43fe2 --- /dev/null +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -0,0 +1,162 @@ +': + return $value > $val; + + case '<': + return $value < $val; + + case '<=': + return $value <= $val; + + case '<>': + return $value != $val; + + case '=': + return $value == $val; + } + + return $value >= $val; + } + + private static function splitFormat($sections, $value) + { + // Extract the relevant section depending on whether number is positive, negative, or zero? + // Text not supported yet. + // Here is how the sections apply to various values in Excel: + // 1 section: [POSITIVE/NEGATIVE/ZERO/TEXT] + // 2 sections: [POSITIVE/ZERO/TEXT] [NEGATIVE] + // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO] + // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT] + $cnt = count($sections); + $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . ')\\]/'; + $cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/'; + $colors = ['', '', '', '', '']; + $condops = ['', '', '', '', '']; + $condvals = [0, 0, 0, 0, 0]; + 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]); + } + if (preg_match($cond_regex, $sections[$idx], $matches)) { + $condops[$idx] = $matches[1]; + $condvals[$idx] = $matches[2]; + $sections[$idx] = preg_replace($cond_regex, '', $sections[$idx]); + } + } + $color = $colors[0]; + $format = $sections[0]; + $absval = $value; + switch ($cnt) { + case 2: + $absval = abs($value); + if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>=', 0)) { + $color = $colors[1]; + $format = $sections[1]; + } + + break; + case 3: + case 4: + $absval = abs($value); + if (!self::splitFormatCompare($value, $condops[0], $condvals[0], '>', 0)) { + if (self::splitFormatCompare($value, $condops[1], $condvals[1], '<', 0)) { + $color = $colors[1]; + $format = $sections[1]; + } else { + $color = $colors[2]; + $format = $sections[2]; + } + } + + break; + } + + return [$color, $format, $absval]; + } + + /** + * Convert a value in a pre-defined format to a PHP string. + * + * @param mixed $value Value to format + * @param string $format Format code, see = NumberFormat::FORMAT_* + * @param array $callBack Callback function for additional formatting of string + * + * @return string Formatted string + */ + public static function toFormattedString($value, $format, $callBack = null) + { + // For now we do not treat strings although section 4 of a format code affects strings + if (!is_numeric($value)) { + return $value; + } + + // For 'General' format code, we just pass the value although this is not entirely the way Excel does it, + // it seems to round numbers to a total of 10 digits. + if (($format === NumberFormat::FORMAT_GENERAL) || ($format === NumberFormat::FORMAT_TEXT)) { + return $value; + } + + $format = preg_replace_callback( + '/(["])(?:(?=(\\\\?))\\2.)*?\\1/u', + function ($matches) { + return str_replace('.', chr(0x00), $matches[0]); + }, + $format + ); + + // Convert any other escaped characters to quoted strings, e.g. (\T to "T") + $format = 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); + + [$colors, $format, $value] = self::splitFormat($sections, $value); + + // 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); + + // Let's begin inspecting the format and converting the value to a formatted string + + // Check for date/time characters (not inside quotes) + if (preg_match('/(\[\$[A-Z]*-[0-9A-F]*\])*[hmsdy](?=(?:[^"]|"[^"]*")*$)/miu', $format, $matches)) { + // datetime format + $value = DateFormatter::format($value, $format); + } else { + if (substr($format, 0, 1) === '"' && substr($format, -1, 1) === '"') { + $value = substr($format, 1, -1); + } elseif (preg_match('/[0#, ]%/', $format)) { + // % number format + $value = PercentageFormatter::format($value, $format); + } else { + $value = NumberFormatter::format($value, $format); + } + } + + // Additional formatting provided by callback function + if ($callBack !== null) { + [$writerInstance, $function] = $callBack; + $value = $writerInstance->$function($value, $colors); + } + + $value = str_replace(chr(0x00), '.', $value); + + return $value; + } +} diff --git a/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php new file mode 100644 index 00000000..2b1c7911 --- /dev/null +++ b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -0,0 +1,45 @@ + 0); + + return [ + implode('.', $masks), + implode('.', array_reverse($postDecimalMasks)), + ]; + } + + private static function processComplexNumberFormatMask($number, $mask): string + { + $result = $number; + $maskingBlockCount = preg_match_all('/0+/', $mask, $maskingBlocks, PREG_OFFSET_CAPTURE); + + if ($maskingBlockCount > 1) { + $maskingBlocks = array_reverse($maskingBlocks[0]); + + foreach ($maskingBlocks as $block) { + $size = strlen($block[0]); + $divisor = 10 ** $size; + $offset = $block[1]; + + $blockValue = sprintf("%0{$size}d", fmod($number, $divisor)); + $number = floor($number / $divisor); + $mask = substr_replace($mask, $blockValue, $offset, $size); + } + if ($number > 0) { + $mask = substr_replace($mask, $number, $offset, 0); + } + $result = $mask; + } + + return $result; + } + + private static function complexNumberFormatMask($number, $mask, $splitOnPoint = true): string + { + $sign = ($number < 0.0) ? '-' : ''; + $number = abs($number); + + if ($splitOnPoint && strpos($mask, '.') !== false && strpos($number, '.') !== false) { + $numbers = explode('.', $number); + $masks = explode('.', $mask); + if (count($masks) > 2) { + $masks = self::mergeComplexNumberFormatMasks($numbers, $masks); + } + $integerPart = self::complexNumberFormatMask($numbers[0], $masks[0], false); + $decimalPart = strrev(self::complexNumberFormatMask(strrev($numbers[1]), strrev($masks[1]), false)); + + return "{$sign}{$integerPart}.{$decimalPart}"; + } + + $result = self::processComplexNumberFormatMask($number, $mask); + + return "{$sign}{$result}"; + } + + private static function formatStraightNumericValue($value, $format, array $matches, $useThousands): string + { + $left = $matches[1]; + $dec = $matches[2]; + $right = $matches[3]; + + // minimun width of formatted number (including dot) + $minWidth = strlen($left) + strlen($dec) + strlen($right); + if ($useThousands) { + $value = number_format( + $value, + strlen($right), + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); + + return preg_replace(self::NUMBER_REGEX, $value, $format); + } + + if (preg_match('/[0#]E[+-]0/i', $format)) { + // Scientific format + return sprintf('%5.2E', $value); + } elseif (preg_match('/0([^\d\.]+)0/', $format) || substr_count($format, '.') > 1) { + if ($value == (int) $value && substr_count($format, '.') === 1) { + $value *= 10 ** strlen(explode('.', $format)[1]); + } + + return self::complexNumberFormatMask($value, $format); + } + + $sprintf_pattern = "%0$minWidth." . strlen($right) . 'f'; + $value = sprintf($sprintf_pattern, $value); + + return preg_replace(self::NUMBER_REGEX, $value, $format); + } + + public static function format($value, $format): string + { + // The "_" in this string has already been stripped out, + // so this test is never true. Furthermore, testing + // on Excel shows this format uses Euro symbol, not "EUR". + //if ($format === NumberFormat::FORMAT_CURRENCY_EUR_SIMPLE) { + // return 'EUR ' . sprintf('%1.2f', $value); + //} + + // Some non-number strings are quoted, so we'll get rid of the quotes, likewise any positional * symbols + $format = str_replace(['"', '*'], '', $format); + + // Find out if we need thousands separator + // This is indicated by a comma enclosed by a digit placeholder: + // #,# or 0,0 + $useThousands = preg_match('/(#,#|0,0)/', $format); + if ($useThousands) { + $format = preg_replace('/0,0/', '00', $format); + $format = preg_replace('/#,#/', '##', $format); + } + + // Scale thousands, millions,... + // This is indicated by a number of commas after a digit placeholder: + // #, or 0.0,, + $scale = 1; // same as no scale + $matches = []; + if (preg_match('/(#|0)(,+)/', $format, $matches)) { + $scale = 1000 ** strlen($matches[2]); + + // strip the commas + $format = preg_replace('/0,+/', '0', $format); + $format = preg_replace('/#,+/', '#', $format); + } + + if (preg_match('/#?.*\?\/\?/', $format, $m)) { + if ($value != (int) $value) { + $value = FractionFormatter::format($value, $format); + } + } else { + // Handle the number itself + + // scale number + $value = $value / $scale; + // Strip # + $format = preg_replace('/\\#/', '0', $format); + // Remove locale code [$-###] + $format = preg_replace('/\[\$\-.*\]/', '', $format); + + $n = '/\\[[^\\]]+\\]/'; + $m = preg_replace($n, '', $format); + if (preg_match(self::NUMBER_REGEX, $m, $matches)) { + $value = self::formatStraightNumericValue($value, $format, $matches, $useThousands); + } + } + + if (preg_match('/\[\$(.*)\]/u', $format, $m)) { + // Currency or Accounting + $currencyCode = $m[1]; + [$currencyCode] = explode('-', $currencyCode); + if ($currencyCode == '') { + $currencyCode = StringHelper::getCurrencyCode(); + } + $value = preg_replace('/\[\$([^\]]*)\]/u', $currencyCode, $value); + } + + return $value; + } +} diff --git a/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php new file mode 100644 index 00000000..cf1731ec --- /dev/null +++ b/src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php @@ -0,0 +1,42 @@ + Date: Wed, 17 Mar 2021 12:39:29 +0100 Subject: [PATCH 50/89] Update change log --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86be590b..9b46323f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,8 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed -- Fixed issue with _ spacing character in number format mask corrumpting output from toFormattedString() [Issue 1924#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1924) [PR #1927](https://github.com/PHPOffice/PhpSpreadsheet/pull/1927) +- Fixed issue with percentage formats in number format mask rendered with toFormattedString() [Issue 1929#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #1928](https://github.com/PHPOffice/PhpSpreadsheet/pull/1928) +- Fixed issue with _ spacing character in number format mask corrupting output from toFormattedString() [Issue 1924#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1924) [PR #1927](https://github.com/PHPOffice/PhpSpreadsheet/pull/1927) - Fix for [Issue #1887](https://github.com/PHPOffice/PhpSpreadsheet/issues/1887) - Lose Track of Selected Cells After Save - Fixed issue with Xlsx@listWorksheetInfo not returning any data - Fixed invalid arguments triggering mb_substr() error in LEFT(), MID() and RIGHT() text functions. [Issue #640](https://github.com/PHPOffice/PhpSpreadsheet/issues/640) From 4cd6c7806e77a9a63e2c3ffabc18a5137bc3f7f9 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 17 Mar 2021 18:36:13 +0100 Subject: [PATCH 51/89] Initial unit tests for Document Properties (#1932) * Initial unit tests for Document Properties * Typehinting in the document properties class --- src/PhpSpreadsheet/Document/Properties.php | 205 +++++++----------- .../Document/PropertiesTest.php | 183 ++++++++++++++++ 2 files changed, 264 insertions(+), 124 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Document/PropertiesTest.php diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index d6aff81e..951d334d 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -5,12 +5,20 @@ namespace PhpOffice\PhpSpreadsheet\Document; class Properties { /** constants */ - const PROPERTY_TYPE_BOOLEAN = 'b'; - const PROPERTY_TYPE_INTEGER = 'i'; - const PROPERTY_TYPE_FLOAT = 'f'; - const PROPERTY_TYPE_DATE = 'd'; - const PROPERTY_TYPE_STRING = 's'; - const PROPERTY_TYPE_UNKNOWN = 'u'; + public const PROPERTY_TYPE_BOOLEAN = 'b'; + public const PROPERTY_TYPE_INTEGER = 'i'; + public const PROPERTY_TYPE_FLOAT = 'f'; + public const PROPERTY_TYPE_DATE = 'd'; + public const PROPERTY_TYPE_STRING = 's'; + public const PROPERTY_TYPE_UNKNOWN = 'u'; + + private const VALID_PROPERTY_TYPE_LIST = [ + self::PROPERTY_TYPE_BOOLEAN, + self::PROPERTY_TYPE_INTEGER, + self::PROPERTY_TYPE_FLOAT, + self::PROPERTY_TYPE_DATE, + self::PROPERTY_TYPE_STRING, + ]; /** * Creator. @@ -92,7 +100,7 @@ class Properties /** * Custom Properties. * - * @var string + * @var string[] */ private $customProperties = []; @@ -109,10 +117,8 @@ class Properties /** * Get Creator. - * - * @return string */ - public function getCreator() + public function getCreator(): string { return $this->creator; } @@ -120,11 +126,9 @@ class Properties /** * Set Creator. * - * @param string $creator - * * @return $this */ - public function setCreator($creator) + public function setCreator(string $creator): self { $this->creator = $creator; @@ -133,10 +137,8 @@ class Properties /** * Get Last Modified By. - * - * @return string */ - public function getLastModifiedBy() + public function getLastModifiedBy(): string { return $this->lastModifiedBy; } @@ -144,23 +146,19 @@ class Properties /** * Set Last Modified By. * - * @param string $pValue - * * @return $this */ - public function setLastModifiedBy($pValue) + public function setLastModifiedBy(string $modifier): self { - $this->lastModifiedBy = $pValue; + $this->lastModifiedBy = $modifier; return $this; } /** * Get Created. - * - * @return int */ - public function getCreated() + public function getCreated(): int { return $this->created; } @@ -168,33 +166,31 @@ class Properties /** * Set Created. * - * @param int|string $time + * @param null|int|string $timestamp * * @return $this */ - public function setCreated($time) + public function setCreated($timestamp): self { - if ($time === null) { - $time = time(); - } elseif (is_string($time)) { - if (is_numeric($time)) { - $time = (int) $time; + if ($timestamp === null) { + $timestamp = time(); + } elseif (is_string($timestamp)) { + if (is_numeric($timestamp)) { + $timestamp = (int) $timestamp; } else { - $time = strtotime($time); + $timestamp = strtotime($timestamp); } } - $this->created = $time; + $this->created = $timestamp; return $this; } /** * Get Modified. - * - * @return int */ - public function getModified() + public function getModified(): int { return $this->modified; } @@ -202,33 +198,31 @@ class Properties /** * Set Modified. * - * @param int|string $time + * @param null|int|string $timestamp * * @return $this */ - public function setModified($time) + public function setModified($timestamp): self { - if ($time === null) { - $time = time(); - } elseif (is_string($time)) { - if (is_numeric($time)) { - $time = (int) $time; + if ($timestamp === null) { + $timestamp = time(); + } elseif (is_string($timestamp)) { + if (is_numeric($timestamp)) { + $timestamp = (int) $timestamp; } else { - $time = strtotime($time); + $timestamp = strtotime($timestamp); } } - $this->modified = $time; + $this->modified = $timestamp; return $this; } /** * Get Title. - * - * @return string */ - public function getTitle() + public function getTitle(): string { return $this->title; } @@ -236,11 +230,9 @@ class Properties /** * Set Title. * - * @param string $title - * * @return $this */ - public function setTitle($title) + public function setTitle(string $title): self { $this->title = $title; @@ -249,10 +241,8 @@ class Properties /** * Get Description. - * - * @return string */ - public function getDescription() + public function getDescription(): string { return $this->description; } @@ -260,11 +250,9 @@ class Properties /** * Set Description. * - * @param string $description - * * @return $this */ - public function setDescription($description) + public function setDescription(string $description): self { $this->description = $description; @@ -273,10 +261,8 @@ class Properties /** * Get Subject. - * - * @return string */ - public function getSubject() + public function getSubject(): string { return $this->subject; } @@ -284,11 +270,9 @@ class Properties /** * Set Subject. * - * @param string $subject - * * @return $this */ - public function setSubject($subject) + public function setSubject(string $subject): self { $this->subject = $subject; @@ -297,10 +281,8 @@ class Properties /** * Get Keywords. - * - * @return string */ - public function getKeywords() + public function getKeywords(): string { return $this->keywords; } @@ -308,11 +290,9 @@ class Properties /** * Set Keywords. * - * @param string $keywords - * * @return $this */ - public function setKeywords($keywords) + public function setKeywords(string $keywords): self { $this->keywords = $keywords; @@ -321,10 +301,8 @@ class Properties /** * Get Category. - * - * @return string */ - public function getCategory() + public function getCategory(): string { return $this->category; } @@ -332,11 +310,9 @@ class Properties /** * Set Category. * - * @param string $category - * * @return $this */ - public function setCategory($category) + public function setCategory(string $category): self { $this->category = $category; @@ -345,10 +321,8 @@ class Properties /** * Get Company. - * - * @return string */ - public function getCompany() + public function getCompany(): string { return $this->company; } @@ -356,11 +330,9 @@ class Properties /** * Set Company. * - * @param string $company - * * @return $this */ - public function setCompany($company) + public function setCompany(string $company): self { $this->company = $company; @@ -369,10 +341,8 @@ class Properties /** * Get Manager. - * - * @return string */ - public function getManager() + public function getManager(): string { return $this->manager; } @@ -380,11 +350,9 @@ class Properties /** * Set Manager. * - * @param string $manager - * * @return $this */ - public function setManager($manager) + public function setManager(string $manager): self { $this->manager = $manager; @@ -394,33 +362,27 @@ class Properties /** * Get a List of Custom Property Names. * - * @return array of string + * @return string[] */ - public function getCustomProperties() + public function getCustomProperties(): array { return array_keys($this->customProperties); } /** * Check if a Custom Property is defined. - * - * @param string $propertyName - * - * @return bool */ - public function isCustomPropertySet($propertyName) + public function isCustomPropertySet(string $propertyName): bool { - return isset($this->customProperties[$propertyName]); + return array_key_exists($propertyName, $this->customProperties); } /** * Get a Custom Property Value. * - * @param string $propertyName - * * @return mixed */ - public function getCustomPropertyValue($propertyName) + public function getCustomPropertyValue(string $propertyName) { if (isset($this->customProperties[$propertyName])) { return $this->customProperties[$propertyName]['value']; @@ -430,24 +392,36 @@ class Properties /** * Get a Custom Property Type. * - * @param string $propertyName - * * @return string */ - public function getCustomPropertyType($propertyName) + public function getCustomPropertyType(string $propertyName) { if (isset($this->customProperties[$propertyName])) { return $this->customProperties[$propertyName]['type']; } } + private function identifyPropertyType($propertyValue) + { + if ($propertyValue === null) { + return self::PROPERTY_TYPE_STRING; + } elseif (is_float($propertyValue)) { + return self::PROPERTY_TYPE_FLOAT; + } elseif (is_int($propertyValue)) { + return self::PROPERTY_TYPE_INTEGER; + } elseif (is_bool($propertyValue)) { + return self::PROPERTY_TYPE_BOOLEAN; + } + + return self::PROPERTY_TYPE_STRING; + } + /** * Set a Custom Property. * - * @param string $propertyName * @param mixed $propertyValue * @param string $propertyType - * 'i' : Integer + * 'i' : Integer * 'f' : Floating Point * 's' : String * 'd' : Date/Time @@ -455,27 +429,10 @@ class Properties * * @return $this */ - public function setCustomProperty($propertyName, $propertyValue = '', $propertyType = null) + public function setCustomProperty(string $propertyName, $propertyValue = '', $propertyType = null): self { - if ( - ($propertyType === null) || (!in_array($propertyType, [self::PROPERTY_TYPE_INTEGER, - self::PROPERTY_TYPE_FLOAT, - self::PROPERTY_TYPE_STRING, - self::PROPERTY_TYPE_DATE, - self::PROPERTY_TYPE_BOOLEAN, - ])) - ) { - if ($propertyValue === null) { - $propertyType = self::PROPERTY_TYPE_STRING; - } elseif (is_float($propertyValue)) { - $propertyType = self::PROPERTY_TYPE_FLOAT; - } elseif (is_int($propertyValue)) { - $propertyType = self::PROPERTY_TYPE_INTEGER; - } elseif (is_bool($propertyValue)) { - $propertyType = self::PROPERTY_TYPE_BOOLEAN; - } else { - $propertyType = self::PROPERTY_TYPE_STRING; - } + if (($propertyType === null) || (!in_array($propertyType, self::VALID_PROPERTY_TYPE_LIST))) { + $propertyType = $this->identifyPropertyType($propertyValue); } $this->customProperties[$propertyName] = [ @@ -501,7 +458,7 @@ class Properties } } - public static function convertProperty($propertyValue, $propertyType) + public static function convertProperty($propertyValue, string $propertyType) { switch ($propertyType) { case 'empty': // Empty @@ -552,7 +509,7 @@ class Properties return $propertyValue; } - public static function convertPropertyType($propertyType) + public static function convertPropertyType(string $propertyType): string { switch ($propertyType) { case 'i1': // 1-Byte Signed Integer diff --git a/tests/PhpSpreadsheetTests/Document/PropertiesTest.php b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php new file mode 100644 index 00000000..567cf620 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Document/PropertiesTest.php @@ -0,0 +1,183 @@ +properties = new Properties(); + } + + public function testNewInstance(): void + { + $createdTime = $modifiedTime = time(); + self::assertSame('Unknown Creator', $this->properties->getCreator()); + self::assertSame('Unknown Creator', $this->properties->getLastModifiedBy()); + self::assertSame('Untitled Spreadsheet', $this->properties->getTitle()); + self::assertSame('Microsoft Corporation', $this->properties->getCompany()); + self::assertSame($createdTime, $this->properties->getCreated()); + self::assertSame($modifiedTime, $this->properties->getModified()); + } + + public function testSetCreator(): void + { + $creator = 'Mark Baker'; + + $this->properties->setCreator($creator); + self::assertSame($creator, $this->properties->getCreator()); + } + + /** + * @dataProvider providerCreationTime + * + * @param mixed $expectedCreationTime + * @param mixed $created + */ + public function testSetCreated($expectedCreationTime, $created): void + { + $expectedCreationTime = $expectedCreationTime ?? time(); + + $this->properties->setCreated($created); + self::assertSame($expectedCreationTime, $this->properties->getCreated()); + } + + public function providerCreationTime(): array + { + return [ + [null, null], + [1615980600, 1615980600], + [1615980600, '1615980600'], + [1615980600, '2021-03-17 11:30:00Z'], + ]; + } + + public function testSetModifier(): void + { + $creator = 'Mark Baker'; + + $this->properties->setLastModifiedBy($creator); + self::assertSame($creator, $this->properties->getLastModifiedBy()); + } + + /** + * @dataProvider providerModifiedTime + * + * @param mixed $expectedModifiedTime + * @param mixed $modified + */ + public function testSetModified($expectedModifiedTime, $modified): void + { + $expectedModifiedTime = $expectedModifiedTime ?? time(); + + $this->properties->setModified($modified); + self::assertSame($expectedModifiedTime, $this->properties->getModified()); + } + + public function providerModifiedTime(): array + { + return [ + [null, null], + [1615980600, 1615980600], + [1615980600, '1615980600'], + [1615980600, '2021-03-17 11:30:00Z'], + ]; + } + + public function testSetTitle(): void + { + $title = 'My spreadsheet title test'; + + $this->properties->setTitle($title); + self::assertSame($title, $this->properties->getTitle()); + } + + public function testSetDescription(): void + { + $description = 'A test for spreadsheet description'; + + $this->properties->setDescription($description); + self::assertSame($description, $this->properties->getDescription()); + } + + public function testSetSubject(): void + { + $subject = 'Test spreadsheet'; + + $this->properties->setSubject($subject); + self::assertSame($subject, $this->properties->getSubject()); + } + + public function testSetKeywords(): void + { + $keywords = 'Test PHPSpreadsheet Spreadsheet Excel LibreOffice Gnumeric OpenSpreadsheetML OASIS'; + + $this->properties->setKeywords($keywords); + self::assertSame($keywords, $this->properties->getKeywords()); + } + + public function testSetCategory(): void + { + $category = 'Testing'; + + $this->properties->setCategory($category); + self::assertSame($category, $this->properties->getCategory()); + } + + public function testSetCompany(): void + { + $company = 'PHPOffice Suite'; + + $this->properties->setCompany($company); + self::assertSame($company, $this->properties->getCompany()); + } + + public function testSetManager(): void + { + $manager = 'Mark Baker'; + + $this->properties->setManager($manager); + self::assertSame($manager, $this->properties->getManager()); + } + + /** + * @dataProvider providerCustomProperties + * + * @param mixed $expectedType + * @param mixed $expectedValue + * @param mixed $propertyName + */ + public function testSetCustomProperties($expectedType, $expectedValue, $propertyName, ...$args): void + { + $this->properties->setCustomProperty($propertyName, ...$args); + self::assertTrue($this->properties->isCustomPropertySet($propertyName)); + self::assertSame($expectedValue, $this->properties->getCustomPropertyValue($propertyName)); + self::assertSame($expectedType, $this->properties->getCustomPropertyType($propertyName)); + } + + public function providerCustomProperties(): array + { + return [ + [Properties::PROPERTY_TYPE_STRING, null, 'Editor', null], + [Properties::PROPERTY_TYPE_STRING, 'Mark Baker', 'Editor', 'Mark Baker'], + [Properties::PROPERTY_TYPE_FLOAT, 1.17, 'Version', 1.17], + [Properties::PROPERTY_TYPE_INTEGER, 2, 'Revision', 2], + [Properties::PROPERTY_TYPE_BOOLEAN, true, 'Tested', true], + [Properties::PROPERTY_TYPE_DATE, '2021-03-17', 'Test Date', '2021-03-17', Properties::PROPERTY_TYPE_DATE], + ]; + } + + public function testGetUnknownCustomProperties(): void + { + $propertyName = 'I DONT EXIST'; + + self::assertFalse($this->properties->isCustomPropertySet($propertyName)); + self::assertNull($this->properties->getCustomPropertyValue($propertyName)); + self::assertNull($this->properties->getCustomPropertyType($propertyName)); + } +} From e59c751276d91c80b63a3194ea9cbe0ea656fa20 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 17 Mar 2021 23:35:44 +0100 Subject: [PATCH 52/89] Fix reference to deprecated transpose in lookup (#1935) * Fix reference to the deprecated `TRANSPOSE()` function in `LOOKUP()`, pointing to the new `transpose()` method in the `LookupRef\Matrix` class instead --- src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php b/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php index 9d75efb0..e21d35dc 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Lookup.php @@ -29,7 +29,7 @@ class Lookup $lookupColumns = self::columnCount($lookupVector); // we correctly orient our results if (($lookupRows === 1 && $lookupColumns > 1) || (!$hasResultVector && $lookupRows === 2 && $lookupColumns !== 2)) { - $lookupVector = LookupRef::TRANSPOSE($lookupVector); + $lookupVector = LookupRef\Matrix::transpose($lookupVector); $lookupRows = self::rowCount($lookupVector); $lookupColumns = self::columnCount($lookupVector); } @@ -84,7 +84,7 @@ class Lookup // we correctly orient our results if ($resultRows === 1 && $resultColumns > 1) { - $resultVector = LookupRef::TRANSPOSE($resultVector); + $resultVector = LookupRef\Matrix::transpose($resultVector); } return $resultVector; From 4e8a926cb42c636c0e60070cdce449bc88bf3ea2 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 19 Mar 2021 18:50:43 +0100 Subject: [PATCH 53/89] Final part of breaking down the Engineering class for Excel Engineering functions into smaller individual/group classes (#1940) * Final breaking down the Engineering class for Excel Engineering functions into smaller individual/group classes * Additional unhappy path tests for Complex Number functions * Fix return docblocks for floats to allow for error strings --- .../Calculation/Calculation.php | 52 +- .../Calculation/Engineering.php | 298 +++++----- .../Calculation/Engineering/Complex.php | 94 ++++ .../Engineering/ComplexFunctions.php | 513 ++++++++++++++++++ .../Engineering/ComplexOperations.php | 120 ++++ .../Calculation/Engineering/Constants.php | 11 + .../data/Calculation/Engineering/COMPLEX.php | 14 +- tests/data/Calculation/Engineering/IMABS.php | 6 + .../Calculation/Engineering/IMAGINARY.php | 6 + .../Calculation/Engineering/IMARGUMENT.php | 6 + .../Calculation/Engineering/IMCONJUGATE.php | 6 + tests/data/Calculation/Engineering/IMCOS.php | 6 + tests/data/Calculation/Engineering/IMCOSH.php | 6 + tests/data/Calculation/Engineering/IMCOT.php | 6 + tests/data/Calculation/Engineering/IMCSC.php | 6 + tests/data/Calculation/Engineering/IMCSCH.php | 6 + tests/data/Calculation/Engineering/IMDIV.php | 9 +- tests/data/Calculation/Engineering/IMEXP.php | 6 + tests/data/Calculation/Engineering/IMLN.php | 8 +- .../data/Calculation/Engineering/IMLOG10.php | 8 +- tests/data/Calculation/Engineering/IMLOG2.php | 8 +- .../data/Calculation/Engineering/IMPOWER.php | 9 +- .../Calculation/Engineering/IMPRODUCT.php | 9 +- tests/data/Calculation/Engineering/IMREAL.php | 6 + tests/data/Calculation/Engineering/IMSEC.php | 6 + tests/data/Calculation/Engineering/IMSECH.php | 6 + tests/data/Calculation/Engineering/IMSIN.php | 6 + tests/data/Calculation/Engineering/IMSINH.php | 6 + tests/data/Calculation/Engineering/IMSQRT.php | 6 + tests/data/Calculation/Engineering/IMSUB.php | 9 +- tests/data/Calculation/Engineering/IMSUM.php | 11 +- tests/data/Calculation/Engineering/IMTAN.php | 6 + 32 files changed, 1082 insertions(+), 193 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Engineering/Complex.php create mode 100644 src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php create mode 100644 src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php create mode 100644 src/PhpSpreadsheet/Calculation/Engineering/Constants.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 5674cb72..871a0b22 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -565,7 +565,7 @@ class Calculation ], 'COMPLEX' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'COMPLEX'], + 'functionCall' => [Engineering\Complex::class, 'COMPLEX'], 'argumentCount' => '2,3', ], 'CONCAT' => [ @@ -1278,127 +1278,127 @@ class Calculation ], 'IMABS' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMABS'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMABS'], 'argumentCount' => '1', ], 'IMAGINARY' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMAGINARY'], + 'functionCall' => [Engineering\Complex::class, 'IMAGINARY'], 'argumentCount' => '1', ], 'IMARGUMENT' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMARGUMENT'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMARGUMENT'], 'argumentCount' => '1', ], 'IMCONJUGATE' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMCONJUGATE'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCONJUGATE'], 'argumentCount' => '1', ], 'IMCOS' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMCOS'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOS'], 'argumentCount' => '1', ], 'IMCOSH' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMCOSH'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOSH'], 'argumentCount' => '1', ], 'IMCOT' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMCOT'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCOT'], 'argumentCount' => '1', ], 'IMCSC' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMCSC'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSC'], 'argumentCount' => '1', ], 'IMCSCH' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMCSCH'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMCSCH'], 'argumentCount' => '1', ], 'IMDIV' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMDIV'], + 'functionCall' => [Engineering\ComplexOperations::class, 'IMDIV'], 'argumentCount' => '2', ], 'IMEXP' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMEXP'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMEXP'], 'argumentCount' => '1', ], 'IMLN' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMLN'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLN'], 'argumentCount' => '1', ], 'IMLOG10' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMLOG10'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG10'], 'argumentCount' => '1', ], 'IMLOG2' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMLOG2'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMLOG2'], 'argumentCount' => '1', ], 'IMPOWER' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMPOWER'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMPOWER'], 'argumentCount' => '2', ], 'IMPRODUCT' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMPRODUCT'], + 'functionCall' => [Engineering\ComplexOperations::class, 'IMPRODUCT'], 'argumentCount' => '1+', ], 'IMREAL' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMREAL'], + 'functionCall' => [Engineering\Complex::class, 'IMREAL'], 'argumentCount' => '1', ], 'IMSEC' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMSEC'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSEC'], 'argumentCount' => '1', ], 'IMSECH' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMSECH'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSECH'], 'argumentCount' => '1', ], 'IMSIN' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMSIN'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSIN'], 'argumentCount' => '1', ], 'IMSINH' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMSINH'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSINH'], 'argumentCount' => '1', ], 'IMSQRT' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMSQRT'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMSQRT'], 'argumentCount' => '1', ], 'IMSUB' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMSUB'], + 'functionCall' => [Engineering\ComplexOperations::class, 'IMSUB'], 'argumentCount' => '2', ], 'IMSUM' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMSUM'], + 'functionCall' => [Engineering\ComplexOperations::class, 'IMSUM'], 'argumentCount' => '1+', ], 'IMTAN' => [ 'category' => Category::CATEGORY_ENGINEERING, - 'functionCall' => [Engineering::class, 'IMTAN'], + 'functionCall' => [Engineering\ComplexFunctions::class, 'IMTAN'], 'argumentCount' => '1', ], 'INDEX' => [ diff --git a/src/PhpSpreadsheet/Calculation/Engineering.php b/src/PhpSpreadsheet/Calculation/Engineering.php index f311fe99..229607e2 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering.php +++ b/src/PhpSpreadsheet/Calculation/Engineering.php @@ -3,14 +3,21 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use Complex\Complex; -use Complex\Exception as ComplexException; +use PhpOffice\PhpSpreadsheet\Calculation\Engineering\ComplexFunctions; +use PhpOffice\PhpSpreadsheet\Calculation\Engineering\ComplexOperations; +/** + * @deprecated 1.18.0 + */ class Engineering { /** * EULER. + * + * @deprecated 1.18.0 + * @see Use Engineering\Constants\EULER instead */ - const EULER = 2.71828182845904523536; + public const EULER = 2.71828182845904523536; /** * parseComplex. @@ -552,6 +559,10 @@ class Engineering * Excel Function: * COMPLEX(realNumber,imaginary[,suffix]) * + * @Deprecated 1.18.0 + * + * @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. @@ -561,20 +572,7 @@ class Engineering */ 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_numeric($realNumber)) && (is_numeric($imaginary))) && - (($suffix == 'i') || ($suffix == 'j') || ($suffix == '')) - ) { - $complex = new Complex($realNumber, $imaginary, $suffix); - - return (string) $complex; - } - - return Functions::VALUE(); + return Engineering\Complex::COMPLEX($realNumber, $imaginary, $suffix); } /** @@ -585,16 +583,18 @@ class Engineering * Excel Function: * IMAGINARY(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMAGINARY() method in the Engineering\Complex class instead + * * @param string $complexNumber the complex number for which you want the imaginary * coefficient * - * @return float + * @return float|string */ public static function IMAGINARY($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (new Complex($complexNumber))->getImaginary(); + return Engineering\Complex::IMAGINARY($complexNumber); } /** @@ -605,15 +605,17 @@ class Engineering * Excel Function: * IMREAL(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMREAL() method in the Engineering\Complex class instead + * * @param string $complexNumber the complex number for which you want the real coefficient * - * @return float + * @return float|string */ public static function IMREAL($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (new Complex($complexNumber))->getReal(); + return Engineering\Complex::IMREAL($complexNumber); } /** @@ -624,15 +626,17 @@ class Engineering * Excel Function: * IMABS(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMABS() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the absolute value * - * @return float + * @return float|string */ public static function IMABS($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (new Complex($complexNumber))->abs(); + return ComplexFunctions::IMABS($complexNumber); } /** @@ -644,20 +648,17 @@ class Engineering * Excel Function: * IMARGUMENT(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMARGUMENT() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the argument theta * * @return float|string */ public static function IMARGUMENT($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - $complex = new Complex($complexNumber); - if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::DIV0(); - } - - return $complex->argument(); + return ComplexFunctions::IMARGUMENT($complexNumber); } /** @@ -668,15 +669,17 @@ class Engineering * Excel Function: * IMCONJUGATE(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMARGUMENT() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the conjugate * * @return string */ public static function IMCONJUGATE($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->conjugate(); + return ComplexFunctions::IMCONJUGATE($complexNumber); } /** @@ -687,15 +690,17 @@ class Engineering * Excel Function: * IMCOS(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMCOS() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the cosine * * @return float|string */ public static function IMCOS($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->cos(); + return ComplexFunctions::IMCOS($complexNumber); } /** @@ -706,15 +711,17 @@ class Engineering * Excel Function: * IMCOSH(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMCOSH() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the hyperbolic cosine * * @return float|string */ public static function IMCOSH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->cosh(); + return ComplexFunctions::IMCOSH($complexNumber); } /** @@ -725,15 +732,17 @@ class Engineering * Excel Function: * IMCOT(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMCOT() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the cotangent * * @return float|string */ public static function IMCOT($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->cot(); + return ComplexFunctions::IMCOT($complexNumber); } /** @@ -744,15 +753,17 @@ class Engineering * Excel Function: * IMCSC(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMCSC() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the cosecant * * @return float|string */ public static function IMCSC($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->csc(); + return ComplexFunctions::IMCSC($complexNumber); } /** @@ -763,15 +774,17 @@ class Engineering * Excel Function: * IMCSCH(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMCSCH() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the hyperbolic cosecant * * @return float|string */ public static function IMCSCH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->csch(); + return ComplexFunctions::IMCSCH($complexNumber); } /** @@ -782,15 +795,17 @@ class Engineering * Excel Function: * IMSIN(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMSIN() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the sine * * @return float|string */ public static function IMSIN($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->sin(); + return ComplexFunctions::IMSIN($complexNumber); } /** @@ -801,15 +816,17 @@ class Engineering * Excel Function: * IMSINH(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMSINH() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the hyperbolic sine * * @return float|string */ public static function IMSINH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->sinh(); + return ComplexFunctions::IMSINH($complexNumber); } /** @@ -820,15 +837,17 @@ class Engineering * Excel Function: * IMSEC(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMSEC() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the secant * * @return float|string */ public static function IMSEC($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->sec(); + return ComplexFunctions::IMSEC($complexNumber); } /** @@ -839,15 +858,17 @@ class Engineering * Excel Function: * IMSECH(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMSECH() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the hyperbolic secant * * @return float|string */ public static function IMSECH($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->sech(); + return ComplexFunctions::IMSECH($complexNumber); } /** @@ -858,15 +879,17 @@ class Engineering * Excel Function: * IMTAN(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMTAN() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the tangent * * @return float|string */ public static function IMTAN($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->tan(); + return ComplexFunctions::IMTAN($complexNumber); } /** @@ -877,20 +900,17 @@ class Engineering * Excel Function: * IMSQRT(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMSQRT() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the square root * * @return string */ public static function IMSQRT($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - $theta = self::IMARGUMENT($complexNumber); - if ($theta === Functions::DIV0()) { - return '0'; - } - - return (string) (new Complex($complexNumber))->sqrt(); + return ComplexFunctions::IMSQRT($complexNumber); } /** @@ -901,20 +921,17 @@ class Engineering * Excel Function: * IMLN(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMLN() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the natural logarithm * * @return string */ public static function IMLN($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - $complex = new Complex($complexNumber); - if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::NAN(); - } - - return (string) (new Complex($complexNumber))->ln(); + return ComplexFunctions::IMLN($complexNumber); } /** @@ -925,20 +942,17 @@ class Engineering * Excel Function: * IMLOG10(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMLOG10() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the common logarithm * * @return string */ public static function IMLOG10($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - $complex = new Complex($complexNumber); - if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::NAN(); - } - - return (string) (new Complex($complexNumber))->log10(); + return ComplexFunctions::IMLOG10($complexNumber); } /** @@ -949,20 +963,17 @@ class Engineering * Excel Function: * IMLOG2(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMLOG2() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the base-2 logarithm * * @return string */ public static function IMLOG2($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - $complex = new Complex($complexNumber); - if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { - return Functions::NAN(); - } - - return (string) (new Complex($complexNumber))->log2(); + return ComplexFunctions::IMLOG2($complexNumber); } /** @@ -973,15 +984,17 @@ class Engineering * Excel Function: * IMEXP(complexNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMEXP() method in the Engineering\ComplexFunctions class instead + * * @param string $complexNumber the complex number for which you want the exponential * * @return string */ public static function IMEXP($complexNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - - return (string) (new Complex($complexNumber))->exp(); + return ComplexFunctions::IMEXP($complexNumber); } /** @@ -992,6 +1005,10 @@ class Engineering * Excel Function: * IMPOWER(complexNumber,realNumber) * + * @Deprecated 1.18.0 + * + * @see Use the IMPOWER() method in the Engineering\ComplexFunctions class instead + * * @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 * @@ -999,14 +1016,7 @@ class Engineering */ public static function IMPOWER($complexNumber, $realNumber) { - $complexNumber = Functions::flattenSingleValue($complexNumber); - $realNumber = Functions::flattenSingleValue($realNumber); - - if (!is_numeric($realNumber)) { - return Functions::VALUE(); - } - - return (string) (new Complex($complexNumber))->pow($realNumber); + return ComplexFunctions::IMPOWER($complexNumber, $realNumber); } /** @@ -1017,6 +1027,10 @@ class Engineering * Excel Function: * IMDIV(complexDividend,complexDivisor) * + * @Deprecated 1.18.0 + * + * @see Use the IMDIV() method in the Engineering\ComplexOperations class instead + * * @param string $complexDividend the complex numerator or dividend * @param string $complexDivisor the complex denominator or divisor * @@ -1024,14 +1038,7 @@ class Engineering */ public static function IMDIV($complexDividend, $complexDivisor) { - $complexDividend = Functions::flattenSingleValue($complexDividend); - $complexDivisor = Functions::flattenSingleValue($complexDivisor); - - try { - return (string) (new Complex($complexDividend))->divideby(new Complex($complexDivisor)); - } catch (ComplexException $e) { - return Functions::NAN(); - } + return ComplexOperations::IMDIV($complexDividend, $complexDivisor); } /** @@ -1042,6 +1049,10 @@ class Engineering * Excel Function: * IMSUB(complexNumber1,complexNumber2) * + * @Deprecated 1.18.0 + * + * @see Use the IMSUB() method in the Engineering\ComplexOperations class instead + * * @param string $complexNumber1 the complex number from which to subtract complexNumber2 * @param string $complexNumber2 the complex number to subtract from complexNumber1 * @@ -1049,14 +1060,7 @@ class Engineering */ public static function IMSUB($complexNumber1, $complexNumber2) { - $complexNumber1 = Functions::flattenSingleValue($complexNumber1); - $complexNumber2 = Functions::flattenSingleValue($complexNumber2); - - try { - return (string) (new Complex($complexNumber1))->subtract(new Complex($complexNumber2)); - } catch (ComplexException $e) { - return Functions::NAN(); - } + return ComplexOperations::IMSUB($complexNumber1, $complexNumber2); } /** @@ -1067,26 +1071,17 @@ class Engineering * Excel Function: * IMSUM(complexNumber[,complexNumber[,...]]) * + * @Deprecated 1.18.0 + * + * @see Use the IMSUM() method in the Engineering\ComplexOperations class instead + * * @param string ...$complexNumbers Series of complex numbers to add * * @return string */ public static function IMSUM(...$complexNumbers) { - // Return value - $returnValue = new Complex(0.0); - $aArgs = Functions::flattenArray($complexNumbers); - - try { - // Loop through the arguments - foreach ($aArgs as $complex) { - $returnValue = $returnValue->add(new Complex($complex)); - } - } catch (ComplexException $e) { - return Functions::NAN(); - } - - return (string) $returnValue; + return ComplexOperations::IMSUM(...$complexNumbers); } /** @@ -1097,26 +1092,17 @@ class Engineering * Excel Function: * IMPRODUCT(complexNumber[,complexNumber[,...]]) * + * @Deprecated 1.18.0 + * + * @see Use the IMPRODUCT() method in the Engineering\ComplexOperations class instead + * * @param string ...$complexNumbers Series of complex numbers to multiply * * @return string */ public static function IMPRODUCT(...$complexNumbers) { - // Return value - $returnValue = new Complex(1.0); - $aArgs = Functions::flattenArray($complexNumbers); - - try { - // Loop through the arguments - foreach ($aArgs as $complex) { - $returnValue = $returnValue->multiply(new Complex($complex)); - } - } catch (ComplexException $e) { - return Functions::NAN(); - } - - return (string) $returnValue; + return ComplexOperations::IMPRODUCT(...$complexNumbers); } /** diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Complex.php b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php new file mode 100644 index 00000000..f6429cbd --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php @@ -0,0 +1,94 @@ +getImaginary(); + } + + /** + * IMREAL. + * + * Returns the real coefficient of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMREAL(complexNumber) + * + * @param string $complexNumber the complex number for which you want the real coefficient + * + * @return float|string + */ + public static function IMREAL($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return $complex->getReal(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php b/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php new file mode 100644 index 00000000..3f37f373 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Engineering/ComplexFunctions.php @@ -0,0 +1,513 @@ +abs(); + } + + /** + * IMARGUMENT. + * + * Returns the argument theta of a complex number, i.e. the angle in radians from the real + * axis to the representation of the number in polar coordinates. + * + * Excel Function: + * IMARGUMENT(complexNumber) + * + * @param string $complexNumber the complex number for which you want the argument theta + * + * @return float|string + */ + public static function IMARGUMENT($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return Functions::DIV0(); + } + + return $complex->argument(); + } + + /** + * IMCONJUGATE. + * + * Returns the complex conjugate of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCONJUGATE(complexNumber) + * + * @param string $complexNumber the complex number for which you want the conjugate + * + * @return string + */ + public static function IMCONJUGATE($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->conjugate(); + } + + /** + * IMCOS. + * + * Returns the cosine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOS(complexNumber) + * + * @param string $complexNumber the complex number for which you want the cosine + * + * @return float|string + */ + public static function IMCOS($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->cos(); + } + + /** + * IMCOSH. + * + * Returns the hyperbolic cosine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOSH(complexNumber) + * + * @param string $complexNumber the complex number for which you want the hyperbolic cosine + * + * @return float|string + */ + public static function IMCOSH($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->cosh(); + } + + /** + * IMCOT. + * + * Returns the cotangent of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCOT(complexNumber) + * + * @param string $complexNumber the complex number for which you want the cotangent + * + * @return float|string + */ + public static function IMCOT($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->cot(); + } + + /** + * IMCSC. + * + * Returns the cosecant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCSC(complexNumber) + * + * @param string $complexNumber the complex number for which you want the cosecant + * + * @return float|string + */ + public static function IMCSC($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->csc(); + } + + /** + * IMCSCH. + * + * Returns the hyperbolic cosecant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMCSCH(complexNumber) + * + * @param string $complexNumber the complex number for which you want the hyperbolic cosecant + * + * @return float|string + */ + public static function IMCSCH($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->csch(); + } + + /** + * IMSIN. + * + * Returns the sine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSIN(complexNumber) + * + * @param string $complexNumber the complex number for which you want the sine + * + * @return float|string + */ + public static function IMSIN($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->sin(); + } + + /** + * IMSINH. + * + * Returns the hyperbolic sine of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSINH(complexNumber) + * + * @param string $complexNumber the complex number for which you want the hyperbolic sine + * + * @return float|string + */ + public static function IMSINH($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->sinh(); + } + + /** + * IMSEC. + * + * Returns the secant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSEC(complexNumber) + * + * @param string $complexNumber the complex number for which you want the secant + * + * @return float|string + */ + public static function IMSEC($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->sec(); + } + + /** + * IMSECH. + * + * Returns the hyperbolic secant of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSECH(complexNumber) + * + * @param string $complexNumber the complex number for which you want the hyperbolic secant + * + * @return float|string + */ + public static function IMSECH($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->sech(); + } + + /** + * IMTAN. + * + * Returns the tangent of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMTAN(complexNumber) + * + * @param string $complexNumber the complex number for which you want the tangent + * + * @return float|string + */ + public static function IMTAN($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->tan(); + } + + /** + * IMSQRT. + * + * Returns the square root of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMSQRT(complexNumber) + * + * @param string $complexNumber the complex number for which you want the square root + * + * @return string + */ + public static function IMSQRT($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + $theta = self::IMARGUMENT($complexNumber); + if ($theta === Functions::DIV0()) { + return '0'; + } + + return (string) $complex->sqrt(); + } + + /** + * IMLN. + * + * Returns the natural logarithm of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLN(complexNumber) + * + * @param string $complexNumber the complex number for which you want the natural logarithm + * + * @return string + */ + public static function IMLN($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return Functions::NAN(); + } + + return (string) $complex->ln(); + } + + /** + * IMLOG10. + * + * Returns the common logarithm (base 10) of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLOG10(complexNumber) + * + * @param string $complexNumber the complex number for which you want the common logarithm + * + * @return string + */ + public static function IMLOG10($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return Functions::NAN(); + } + + return (string) $complex->log10(); + } + + /** + * IMLOG2. + * + * Returns the base-2 logarithm of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMLOG2(complexNumber) + * + * @param string $complexNumber the complex number for which you want the base-2 logarithm + * + * @return string + */ + public static function IMLOG2($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + if ($complex->getReal() == 0.0 && $complex->getImaginary() == 0.0) { + return Functions::NAN(); + } + + return (string) $complex->log2(); + } + + /** + * IMEXP. + * + * Returns the exponential of a complex number in x + yi or x + yj text format. + * + * Excel Function: + * IMEXP(complexNumber) + * + * @param string $complexNumber the complex number for which you want the exponential + * + * @return string + */ + public static function IMEXP($complexNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $complex->exp(); + } + + /** + * IMPOWER. + * + * Returns a complex number in x + yi or x + yj text format raised to a power. + * + * 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 + * + * @return string + */ + public static function IMPOWER($complexNumber, $realNumber) + { + $complexNumber = Functions::flattenSingleValue($complexNumber); + $realNumber = Functions::flattenSingleValue($realNumber); + + try { + $complex = new ComplexObject($complexNumber); + } catch (ComplexException $e) { + return Functions::NAN(); + } + + if (!is_numeric($realNumber)) { + return Functions::VALUE(); + } + + return (string) $complex->pow($realNumber); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php b/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php new file mode 100644 index 00000000..681aad8c --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Engineering/ComplexOperations.php @@ -0,0 +1,120 @@ +divideby(new ComplexObject($complexDivisor)); + } catch (ComplexException $e) { + return Functions::NAN(); + } + } + + /** + * IMSUB. + * + * Returns the difference of two complex numbers in x + yi or x + yj text format. + * + * 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 + * + * @return string + */ + public static function IMSUB($complexNumber1, $complexNumber2) + { + $complexNumber1 = Functions::flattenSingleValue($complexNumber1); + $complexNumber2 = Functions::flattenSingleValue($complexNumber2); + + try { + return (string) (new ComplexObject($complexNumber1))->subtract(new ComplexObject($complexNumber2)); + } catch (ComplexException $e) { + return Functions::NAN(); + } + } + + /** + * IMSUM. + * + * Returns the sum of two or more complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMSUM(complexNumber[,complexNumber[,...]]) + * + * @param string ...$complexNumbers Series of complex numbers to add + * + * @return string + */ + public static function IMSUM(...$complexNumbers) + { + // Return value + $returnValue = new ComplexObject(0.0); + $aArgs = Functions::flattenArray($complexNumbers); + + try { + // Loop through the arguments + foreach ($aArgs as $complex) { + $returnValue = $returnValue->add(new ComplexObject($complex)); + } + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $returnValue; + } + + /** + * IMPRODUCT. + * + * Returns the product of two or more complex numbers in x + yi or x + yj text format. + * + * Excel Function: + * IMPRODUCT(complexNumber[,complexNumber[,...]]) + * + * @param string ...$complexNumbers Series of complex numbers to multiply + * + * @return string + */ + public static function IMPRODUCT(...$complexNumbers) + { + // Return value + $returnValue = new ComplexObject(1.0); + $aArgs = Functions::flattenArray($complexNumbers); + + try { + // Loop through the arguments + foreach ($aArgs as $complex) { + $returnValue = $returnValue->multiply(new ComplexObject($complex)); + } + } catch (ComplexException $e) { + return Functions::NAN(); + } + + return (string) $returnValue; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Constants.php b/src/PhpSpreadsheet/Calculation/Engineering/Constants.php new file mode 100644 index 00000000..a926db6e --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Engineering/Constants.php @@ -0,0 +1,11 @@ + Date: Sat, 20 Mar 2021 18:40:53 +0100 Subject: [PATCH 54/89] Start work on breaking down some of the Financial Excel functions (#1941) * Start work on breaking down some of the Financial Excel functions * Unhappy path unit tests for Treasury Bill functions * Codebase for Treasury Bills includes logic for a different days between settlement and maturity calculation for OpenOffice; but Open/Libre Office now uses the Excel days calculation, so this discrepancy between packages is no longer required * We've already converted the Settlement and Maturity dates to Excel timestamps, so there's no need to try doing it again when calculating the days between Settlement and Maturity * Add Unit Tests for the Days per Year helper function * Extract Interest Rate functions - EFFECT() and NOMINAL() - with additional validation, and unhappy path unit tests * First pass at extracting the Coupon Excel functions * Simplify the validation methods * Extended unit tests to cover all combinations of frequency and basis, including leap years Fix for COUPDAYSNC() when basis is US 360 and settlement date is the last day of the month * Ensure that all Financial function code uses the new Helpers class for Days Per Year --- .../Calculation/Calculation.php | 26 +- src/PhpSpreadsheet/Calculation/Financial.php | 469 ++++-------------- .../Calculation/Financial/Coupons.php | 435 ++++++++++++++++ .../Calculation/Financial/Dollar.php | 80 +++ .../Calculation/Financial/Helpers.php | 50 ++ .../Calculation/Financial/InterestRate.php | 69 +++ .../Calculation/Financial/TreasuryBill.php | 154 ++++++ .../Functions/Financial/EffectTest.php | 6 +- .../Functions/Financial/HelpersTest.php | 27 + .../Functions/Financial/NominalTest.php | 6 +- .../data/Calculation/Financial/COUPDAYBS.php | 163 +++++- tests/data/Calculation/Financial/COUPDAYS.php | 156 +++++- .../data/Calculation/Financial/COUPDAYSNC.php | 177 ++++++- tests/data/Calculation/Financial/COUPNCD.php | 151 +++++- tests/data/Calculation/Financial/COUPNUM.php | 156 +++++- tests/data/Calculation/Financial/COUPPCD.php | 151 +++++- .../Calculation/Financial/DaysPerYear.php | 15 + tests/data/Calculation/Financial/EFFECT.php | 21 +- tests/data/Calculation/Financial/NOMINAL.php | 21 +- tests/data/Calculation/Financial/TBILLEQ.php | 32 +- .../data/Calculation/Financial/TBILLPRICE.php | 36 +- .../data/Calculation/Financial/TBILLYIELD.php | 26 + 22 files changed, 2009 insertions(+), 418 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Coupons.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Dollar.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Helpers.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/InterestRate.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Financial/HelpersTest.php create mode 100644 tests/data/Calculation/Financial/DaysPerYear.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 871a0b22..05fe8a81 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -650,32 +650,32 @@ class Calculation ], 'COUPDAYBS' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'COUPDAYBS'], + 'functionCall' => [Financial\Coupons::class, 'COUPDAYBS'], 'argumentCount' => '3,4', ], 'COUPDAYS' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'COUPDAYS'], + 'functionCall' => [Financial\Coupons::class, 'COUPDAYS'], 'argumentCount' => '3,4', ], 'COUPDAYSNC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'COUPDAYSNC'], + 'functionCall' => [Financial\Coupons::class, 'COUPDAYSNC'], 'argumentCount' => '3,4', ], 'COUPNCD' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'COUPNCD'], + 'functionCall' => [Financial\Coupons::class, 'COUPNCD'], 'argumentCount' => '3,4', ], 'COUPNUM' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'COUPNUM'], + 'functionCall' => [Financial\Coupons::class, 'COUPNUM'], 'argumentCount' => '3,4', ], 'COUPPCD' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'COUPPCD'], + 'functionCall' => [Financial\Coupons::class, 'COUPPCD'], 'argumentCount' => '3,4', ], 'COVAR' => [ @@ -875,12 +875,12 @@ class Calculation ], 'DOLLARDE' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'DOLLARDE'], + 'functionCall' => [Financial\Dollar::class, 'decimal'], 'argumentCount' => '2', ], 'DOLLARFR' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'DOLLARFR'], + 'functionCall' => [Financial\Dollar::class, 'fractional'], 'argumentCount' => '2', ], 'DPRODUCT' => [ @@ -925,7 +925,7 @@ class Calculation ], 'EFFECT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'EFFECT'], + 'functionCall' => [Financial\InterestRate::class, 'effective'], 'argumentCount' => '2', ], 'ENCODEURL' => [ @@ -1771,7 +1771,7 @@ class Calculation ], 'NOMINAL' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'NOMINAL'], + 'functionCall' => [Financial\InterestRate::class, 'nominal'], 'argumentCount' => '2', ], 'NORMDIST' => [ @@ -2376,17 +2376,17 @@ class Calculation ], 'TBILLEQ' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'TBILLEQ'], + 'functionCall' => [Financial\TreasuryBill::class, 'bondEquivalentYield'], 'argumentCount' => '3', ], 'TBILLPRICE' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'TBILLPRICE'], + 'functionCall' => [Financial\TreasuryBill::class, 'price'], 'argumentCount' => '3', ], 'TBILLYIELD' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'TBILLYIELD'], + 'functionCall' => [Financial\TreasuryBill::class, 'yield'], 'argumentCount' => '3', ], 'TDIST' => [ diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index f0b5ab05..f6f5c775 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -2,7 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; -use PhpOffice\PhpSpreadsheet\Shared\Date; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\InterestRate; class Financial { @@ -10,41 +11,6 @@ class Financial const FINANCIAL_PRECISION = 1.0e-08; - /** - * isLastDayOfMonth. - * - * Returns a boolean TRUE/FALSE indicating if this date is the last date of the month - * - * @param \DateTime $testDate The date for testing - * - * @return bool - */ - private static function isLastDayOfMonth(\DateTime $testDate) - { - return $testDate->format('d') == $testDate->format('t'); - } - - private static function couponFirstPeriodDate($settlement, $maturity, $frequency, $next) - { - $months = 12 / $frequency; - - $result = Date::excelToDateTimeObject($maturity); - $eom = self::isLastDayOfMonth($result); - - while ($settlement < Date::PHPToExcel($result)) { - $result->modify('-' . $months . ' months'); - } - if ($next) { - $result->modify('+' . $months . ' months'); - } - - if ($eom) { - $result->modify('-1 day'); - } - - return Date::PHPToExcel($result); - } - private static function isValidFrequency($frequency) { if (($frequency == 1) || ($frequency == 2) || ($frequency == 4)) { @@ -54,45 +20,6 @@ class Financial return false; } - /** - * daysPerYear. - * - * Returns the number of days in a specified year, as defined by the "basis" value - * - * @param int|string $year The year against which we're testing - * @param int|string $basis The type of day count: - * 0 or omitted US (NASD) 360 - * 1 Actual (365 or 366 in a leap year) - * 2 360 - * 3 365 - * 4 European 360 - * - * @return int|string Result, or a string containing an error - */ - private static function daysPerYear($year, $basis = 0) - { - switch ($basis) { - case 0: - case 2: - case 4: - $daysPerYear = 360; - - break; - case 3: - $daysPerYear = 365; - - break; - case 1: - $daysPerYear = (DateTime::isLeapYear($year)) ? 366 : 365; - - break; - default: - return Functions::NAN(); - } - - return $daysPerYear; - } - private static function interestAndPrincipal($rate = 0, $per = 0, $nper = 0, $pv = 0, $fv = 0, $type = 0) { $pmt = self::PMT($rate, $nper, $pv, $fv, $type); @@ -369,6 +296,10 @@ class Financial * Excel Function: * COUPDAYBS(settlement,maturity,frequency[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the COUPDAYBS() method in the Financial\Coupons class instead + * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue * date when the security is traded to the buyer. @@ -390,34 +321,7 @@ class Financial */ public static function COUPDAYBS($settlement, $maturity, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $frequency = (int) Functions::flattenSingleValue($frequency); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - - if (is_string($settlement = DateTime::getDateValue($settlement))) { - return Functions::VALUE(); - } - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - if ( - ($settlement >= $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - $daysPerYear = self::daysPerYear(DateTime::YEAR($settlement), $basis); - $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, false); - - if ($basis == 1) { - return abs(DateTime::DAYS($prev, $settlement)); - } - - return DateTime::YEARFRAC($prev, $settlement, $basis) * $daysPerYear; + return Coupons::COUPDAYBS($settlement, $maturity, $frequency, $basis); } /** @@ -428,6 +332,10 @@ class Financial * Excel Function: * COUPDAYS(settlement,maturity,frequency[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the COUPDAYS() method in the Financial\Coupons class instead + * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue * date when the security is traded to the buyer. @@ -449,45 +357,7 @@ class Financial */ public static function COUPDAYS($settlement, $maturity, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $frequency = (int) Functions::flattenSingleValue($frequency); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - - if (is_string($settlement = DateTime::getDateValue($settlement))) { - return Functions::VALUE(); - } - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - if ( - ($settlement >= $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - switch ($basis) { - case 3: - // Actual/365 - return 365 / $frequency; - case 1: - // Actual/actual - if ($frequency == 1) { - $daysPerYear = self::daysPerYear(DateTime::YEAR($settlement), $basis); - - return $daysPerYear / $frequency; - } - $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, false); - $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, true); - - return $next - $prev; - default: - // US (NASD) 30/360, Actual/360 or European 30/360 - return 360 / $frequency; - } + return Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis); } /** @@ -498,6 +368,10 @@ class Financial * Excel Function: * COUPDAYSNC(settlement,maturity,frequency[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the COUPDAYSNC() method in the Financial\Coupons class instead + * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue * date when the security is traded to the buyer. @@ -519,30 +393,7 @@ class Financial */ public static function COUPDAYSNC($settlement, $maturity, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $frequency = (int) Functions::flattenSingleValue($frequency); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - - if (is_string($settlement = DateTime::getDateValue($settlement))) { - return Functions::VALUE(); - } - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - if ( - ($settlement >= $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - $daysPerYear = self::daysPerYear(DateTime::YEAR($settlement), $basis); - $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, true); - - return DateTime::YEARFRAC($settlement, $next, $basis) * $daysPerYear; + return Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis); } /** @@ -553,6 +404,10 @@ class Financial * Excel Function: * COUPNCD(settlement,maturity,frequency[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the COUPNCD() method in the Financial\Coupons class instead + * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue * date when the security is traded to the buyer. @@ -575,27 +430,7 @@ class Financial */ public static function COUPNCD($settlement, $maturity, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $frequency = (int) Functions::flattenSingleValue($frequency); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - - if (is_string($settlement = DateTime::getDateValue($settlement))) { - return Functions::VALUE(); - } - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - if ( - ($settlement >= $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - return self::couponFirstPeriodDate($settlement, $maturity, $frequency, true); + return Coupons::COUPNCD($settlement, $maturity, $frequency, $basis); } /** @@ -607,6 +442,10 @@ class Financial * Excel Function: * COUPNUM(settlement,maturity,frequency[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the COUPNUM() method in the Financial\Coupons class instead + * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue * date when the security is traded to the buyer. @@ -628,29 +467,7 @@ class Financial */ public static function COUPNUM($settlement, $maturity, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $frequency = (int) Functions::flattenSingleValue($frequency); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - - if (is_string($settlement = DateTime::getDateValue($settlement))) { - return Functions::VALUE(); - } - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - if ( - ($settlement >= $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - $yearsBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, 0); - - return ceil($yearsBetweenSettlementAndMaturity * $frequency); + return Coupons::COUPNUM($settlement, $maturity, $frequency, $basis); } /** @@ -661,6 +478,10 @@ class Financial * Excel Function: * COUPPCD(settlement,maturity,frequency[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the COUPPCD() method in the Financial\Coupons class instead + * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue * date when the security is traded to the buyer. @@ -683,27 +504,7 @@ class Financial */ public static function COUPPCD($settlement, $maturity, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $frequency = (int) Functions::flattenSingleValue($frequency); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - - if (is_string($settlement = DateTime::getDateValue($settlement))) { - return Functions::VALUE(); - } - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - if ( - ($settlement >= $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - return self::couponFirstPeriodDate($settlement, $maturity, $frequency, false); + return Coupons::COUPPCD($settlement, $maturity, $frequency, $basis); } /** @@ -997,6 +798,10 @@ class Financial * Excel Function: * DOLLARDE(fractional_dollar,fraction) * + * @Deprecated 1.18.0 + * + * @see Use the decimal() method in the Financial\Dollar class instead + * * @param float $fractional_dollar Fractional Dollar * @param int $fraction Fraction * @@ -1004,23 +809,7 @@ class Financial */ public static function DOLLARDE($fractional_dollar = null, $fraction = 0) { - $fractional_dollar = Functions::flattenSingleValue($fractional_dollar); - $fraction = (int) Functions::flattenSingleValue($fraction); - - // Validate parameters - if ($fractional_dollar === null || $fraction < 0) { - return Functions::NAN(); - } - if ($fraction == 0) { - return Functions::DIV0(); - } - - $dollars = floor($fractional_dollar); - $cents = fmod($fractional_dollar, 1); - $cents /= $fraction; - $cents *= 10 ** ceil(log10($fraction)); - - return $dollars + $cents; + return Financial\Dollar::decimal($fractional_dollar, $fraction); } /** @@ -1033,6 +822,10 @@ class Financial * Excel Function: * DOLLARFR(decimal_dollar,fraction) * + * @Deprecated 1.18.0 + * + * @see Use the fractional() method in the Financial\Dollar class instead + * * @param float $decimal_dollar Decimal Dollar * @param int $fraction Fraction * @@ -1040,23 +833,7 @@ class Financial */ public static function DOLLARFR($decimal_dollar = null, $fraction = 0) { - $decimal_dollar = Functions::flattenSingleValue($decimal_dollar); - $fraction = (int) Functions::flattenSingleValue($fraction); - - // Validate parameters - if ($decimal_dollar === null || $fraction < 0) { - return Functions::NAN(); - } - if ($fraction == 0) { - return Functions::DIV0(); - } - - $dollars = floor($decimal_dollar); - $cents = fmod($decimal_dollar, 1); - $cents *= $fraction; - $cents *= 10 ** (-ceil(log10($fraction))); - - return $dollars + $cents; + return Financial\Dollar::fractional($decimal_dollar, $fraction); } /** @@ -1068,22 +845,18 @@ class Financial * Excel Function: * EFFECT(nominal_rate,npery) * - * @param float $nominal_rate Nominal interest rate - * @param int $npery Number of compounding payments per year + * @Deprecated 1.18.0 + * + * @see Use the effective() method in the Financial\InterestRate class instead + * + * @param float $nominalRate Nominal interest rate + * @param int $periodsPerYear Number of compounding payments per year * * @return float|string */ - public static function EFFECT($nominal_rate = 0, $npery = 0) + public static function EFFECT($nominalRate = 0, $periodsPerYear = 0) { - $nominal_rate = Functions::flattenSingleValue($nominal_rate); - $npery = (int) Functions::flattenSingleValue($npery); - - // Validate parameters - if ($nominal_rate <= 0 || $npery < 1) { - return Functions::NAN(); - } - - return (1 + $nominal_rate / $npery) ** $npery - 1; + return Financial\InterestRate::effective($nominalRate, $periodsPerYear); } /** @@ -1412,23 +1185,21 @@ class Financial * * Returns the nominal interest rate given the effective rate and the number of compounding payments per year. * - * @param float $effect_rate Effective interest rate - * @param int $npery Number of compounding payments per year + * Excel Function: + * NOMINAL(effect_rate, npery) + * + * @Deprecated 1.18.0 + * + * @see Use the nominal() method in the Financial\InterestRate class instead + * + * @param float $effectiveRate Effective interest rate + * @param int $periodsPerYear Number of compounding payments per year * * @return float|string Result, or a string containing an error */ - public static function NOMINAL($effect_rate = 0, $npery = 0) + public static function NOMINAL($effectiveRate = 0, $periodsPerYear = 0) { - $effect_rate = Functions::flattenSingleValue($effect_rate); - $npery = (int) Functions::flattenSingleValue($npery); - - // Validate parameters - if ($effect_rate <= 0 || $npery < 1) { - return Functions::NAN(); - } - - // Calculate - return $npery * (($effect_rate + 1) ** (1 / $npery) - 1); + return InterestRate::nominal($effectiveRate, $periodsPerYear); } /** @@ -1754,7 +1525,7 @@ class Financial if (($rate <= 0) || ($yield <= 0)) { return Functions::NAN(); } - $daysPerYear = self::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); if (!is_numeric($daysPerYear)) { return $daysPerYear; } @@ -2030,6 +1801,10 @@ class Financial * * Returns the bond-equivalent yield for a Treasury bill. * + * @Deprecated 1.18.0 + * + * @see Use the bondEquivalentYield() method in the Financial\TreasuryBill class instead + * * @param mixed $settlement The Treasury bill's settlement date. * The Treasury bill's settlement date is the date after the issue date when the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. @@ -2040,37 +1815,21 @@ class Financial */ public static function TBILLEQ($settlement, $maturity, $discount) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $discount = Functions::flattenSingleValue($discount); - - // Use TBILLPRICE for validation - $testValue = self::TBILLPRICE($settlement, $maturity, $discount); - if (is_string($testValue)) { - return $testValue; - } - - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { - ++$maturity; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity) * 360; - } else { - $daysBetweenSettlementAndMaturity = (DateTime::getDateValue($maturity) - DateTime::getDateValue($settlement)); - } - - return (365 * $discount) / (360 - $discount * $daysBetweenSettlementAndMaturity); + return Financial\TreasuryBill::bondEquivalentYield($settlement, $maturity, $discount); } /** * TBILLPRICE. * - * Returns the yield for a Treasury bill. + * Returns the price per $100 face value for a Treasury bill. + * + * @Deprecated 1.18.0 + * + * @see Use the price() method in the Financial\TreasuryBill class instead * * @param mixed $settlement The Treasury bill's settlement date. - * The Treasury bill's settlement date is the date after the issue date when the Treasury bill is traded to the buyer. + * The Treasury bill's settlement date is the date after the issue date + * when the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. * @param int $discount The Treasury bill's discount rate @@ -2079,44 +1838,7 @@ class Financial */ public static function TBILLPRICE($settlement, $maturity, $discount) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $discount = Functions::flattenSingleValue($discount); - - if (is_string($maturity = DateTime::getDateValue($maturity))) { - return Functions::VALUE(); - } - - // Validate - if (is_numeric($discount)) { - if ($discount <= 0) { - return Functions::NAN(); - } - - if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { - ++$maturity; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity) * 360; - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - } else { - $daysBetweenSettlementAndMaturity = (DateTime::getDateValue($maturity) - DateTime::getDateValue($settlement)); - } - - if ($daysBetweenSettlementAndMaturity > self::daysPerYear(DateTime::YEAR($maturity), 1)) { - return Functions::NAN(); - } - - $price = 100 * (1 - (($discount * $daysBetweenSettlementAndMaturity) / 360)); - if ($price <= 0) { - return Functions::NAN(); - } - - return $price; - } - - return Functions::VALUE(); + return Financial\TreasuryBill::price($settlement, $maturity, $discount); } /** @@ -2124,8 +1846,13 @@ class Financial * * Returns the yield for a Treasury bill. * + * @Deprecated 1.18.0 + * + * @see Use the yield() method in the Financial\TreasuryBill class instead + * * @param mixed $settlement The Treasury bill's settlement date. - * The Treasury bill's settlement date is the date after the issue date when the Treasury bill is traded to the buyer. + * The Treasury bill's settlement date is the date after the issue date + * when the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. * @param int $price The Treasury bill's price per $100 face value @@ -2134,35 +1861,7 @@ class Financial */ public static function TBILLYIELD($settlement, $maturity, $price) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $price = Functions::flattenSingleValue($price); - - // Validate - if (is_numeric($price)) { - if ($price <= 0) { - return Functions::NAN(); - } - - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE) { - ++$maturity; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity) * 360; - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - } else { - $daysBetweenSettlementAndMaturity = (DateTime::getDateValue($maturity) - DateTime::getDateValue($settlement)); - } - - if ($daysBetweenSettlementAndMaturity > 360) { - return Functions::NAN(); - } - - return ((100 - $price) / $price) * (360 / $daysBetweenSettlementAndMaturity); - } - - return Functions::VALUE(); + return Financial\TreasuryBill::yield($settlement, $maturity, $price); } private static function bothNegAndPos($neg, $pos) @@ -2407,7 +2106,7 @@ class Financial if (($price <= 0) || ($redemption <= 0)) { return Functions::NAN(); } - $daysPerYear = self::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); if (!is_numeric($daysPerYear)) { return $daysPerYear; } @@ -2459,7 +2158,7 @@ class Financial if (($rate <= 0) || ($price <= 0)) { return Functions::NAN(); } - $daysPerYear = self::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); if (!is_numeric($daysPerYear)) { return $daysPerYear; } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php new file mode 100644 index 00000000..ff99c839 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -0,0 +1,435 @@ +getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); + + if ($basis === Helpers::DAYS_PER_YEAR_ACTUAL) { + return abs(DateTime::DAYS($prev, $settlement)); + } + + return DateTime::YEARFRAC($prev, $settlement, $basis) * $daysPerYear; + } + + /** + * COUPDAYS. + * + * Returns the number of days in the coupon period that contains the settlement date. + * + * Excel Function: + * COUPDAYS(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency the number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string + */ + public static function COUPDAYS($settlement, $maturity, $frequency, $basis = Helpers::DAYS_PER_YEAR_NASD) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = self::validateFrequency($frequency); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + switch ($basis) { + case Helpers::DAYS_PER_YEAR_365: + // Actual/365 + return 365 / $frequency; + case Helpers::DAYS_PER_YEAR_ACTUAL: + // Actual/actual + if ($frequency == self::FREQUENCY_ANNUAL) { + $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + + return $daysPerYear / $frequency; + } + $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); + $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); + + return $next - $prev; + default: + // US (NASD) 30/360, Actual/360 or European 30/360 + return 360 / $frequency; + } + } + + /** + * COUPDAYSNC. + * + * Returns the number of days from the settlement date to the next coupon date. + * + * Excel Function: + * COUPDAYSNC(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency the number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string + */ + public static function COUPDAYSNC($settlement, $maturity, $frequency, $basis = Helpers::DAYS_PER_YEAR_NASD) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = self::validateFrequency($frequency); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); + + if ($basis === Helpers::DAYS_PER_YEAR_NASD) { + $settlementDate = Date::excelToDateTimeObject($settlement); + $settlementEoM = self::isLastDayOfMonth($settlementDate); + if ($settlementEoM) { + ++$settlement; + } + } + + return DateTime::YEARFRAC($settlement, $next, $basis) * $daysPerYear; + } + + /** + * COUPNCD. + * + * Returns the next coupon date after the settlement date. + * + * Excel Function: + * COUPNCD(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency the number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @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 + */ + public static function COUPNCD($settlement, $maturity, $frequency, $basis = Helpers::DAYS_PER_YEAR_NASD) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = self::validateFrequency($frequency); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); + } + + /** + * COUPNUM. + * + * Returns the number of coupons payable between the settlement date and maturity date, + * rounded up to the nearest whole coupon. + * + * Excel Function: + * COUPNUM(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency the number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return int|string + */ + public static function COUPNUM($settlement, $maturity, $frequency, $basis = Helpers::DAYS_PER_YEAR_NASD) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = self::validateFrequency($frequency); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $yearsBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, 0); + + return ceil($yearsBetweenSettlementAndMaturity * $frequency); + } + + /** + * COUPPCD. + * + * Returns the previous coupon date before the settlement date. + * + * Excel Function: + * COUPPCD(settlement,maturity,frequency[,basis]) + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $frequency the number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @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 + */ + public static function COUPPCD($settlement, $maturity, $frequency, $basis = Helpers::DAYS_PER_YEAR_NASD) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $frequency = Functions::flattenSingleValue($frequency); + $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateCouponPeriod($settlement, $maturity); + $frequency = self::validateFrequency($frequency); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); + } + + /** + * isLastDayOfMonth. + * + * Returns a boolean TRUE/FALSE indicating if this date is the last date of the month + * + * @param \DateTime $testDate The date for testing + * + * @return bool + */ + private static function isLastDayOfMonth(\DateTime $testDate) + { + return $testDate->format('d') === $testDate->format('t'); + } + + private static function couponFirstPeriodDate($settlement, $maturity, int $frequency, $next) + { + $months = 12 / $frequency; + + $result = Date::excelToDateTimeObject($maturity); + $maturityEoM = self::isLastDayOfMonth($result); + + while ($settlement < Date::PHPToExcel($result)) { + $result->modify('-' . $months . ' months'); + } + if ($next === true) { + $result->modify('+' . $months . ' months'); + } + + if ($maturityEoM === true) { + $result->modify('-1 day'); + } + + return Date::PHPToExcel($result); + } + + private static function validateInputDate($date) + { + $date = DateTime::getDateValue($date); + if (is_string($date)) { + throw new Exception(Functions::VALUE()); + } + + return $date; + } + + private static function validateSettlementDate($settlement) + { + return self::validateInputDate($settlement); + } + + private static function validateMaturityDate($maturity) + { + return self::validateInputDate($maturity); + } + + private static function validateCouponPeriod($settlement, $maturity): void + { + if ($settlement >= $maturity) { + throw new Exception(Functions::NAN()); + } + } + + private static function validateFrequency($frequency): int + { + if (!is_numeric($frequency)) { + throw new Exception(Functions::NAN()); + } + + $frequency = (int) $frequency; + if ( + ($frequency !== self::FREQUENCY_ANNUAL) && + ($frequency !== self::FREQUENCY_SEMI_ANNUAL) && + ($frequency !== self::FREQUENCY_QUARTERLY) + ) { + throw new Exception(Functions::NAN()); + } + + return $frequency; + } + + private static function validateBasis($basis) + { + if (!is_numeric($basis)) { + throw new Exception(Functions::NAN()); + } + + $basis = (int) $basis; + if (($basis < 0) || ($basis > 4)) { + throw new Exception(Functions::NAN()); + } + + return $basis; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/Dollar.php b/src/PhpSpreadsheet/Calculation/Financial/Dollar.php new file mode 100644 index 00000000..e85b00c6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Dollar.php @@ -0,0 +1,80 @@ + Helpers::daysPerYear(DateTime::YEAR($maturity), Helpers::DAYS_PER_YEAR_ACTUAL) || + $daysBetweenSettlementAndMaturity < 0 + ) { + return Functions::NAN(); + } + + return (365 * $discount) / (360 - $discount * $daysBetweenSettlementAndMaturity); + } + + return Functions::VALUE(); + } + + /** + * TBILLPRICE. + * + * Returns the price per $100 face value for a Treasury bill. + * + * @param mixed $settlement The Treasury bill's settlement date. + * The Treasury bill's settlement date is the date after the issue date + * when the Treasury bill is traded to the buyer. + * @param mixed $maturity The Treasury bill's maturity date. + * The maturity date is the date when the Treasury bill expires. + * @param int $discount The Treasury bill's discount rate + * + * @return float|string Result, or a string containing an error + */ + public static function price($settlement, $maturity, $discount) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $discount = Functions::flattenSingleValue($discount); + + if ( + is_string($maturity = DateTime::getDateValue($maturity)) || + is_string($settlement = DateTime::getDateValue($settlement)) + ) { + return Functions::VALUE(); + } + + // Validate + if (is_numeric($discount)) { + if ($discount <= 0) { + return Functions::NAN(); + } + + $daysBetweenSettlementAndMaturity = $maturity - $settlement; + + if ( + $daysBetweenSettlementAndMaturity > Helpers::daysPerYear(DateTime::YEAR($maturity), Helpers::DAYS_PER_YEAR_ACTUAL) || + $daysBetweenSettlementAndMaturity < 0 + ) { + return Functions::NAN(); + } + $price = 100 * (1 - (($discount * $daysBetweenSettlementAndMaturity) / 360)); + if ($price < 0.0) { + return Functions::NAN(); + } + + return $price; + } + + return Functions::VALUE(); + } + + /** + * TBILLYIELD. + * + * Returns the yield for a Treasury bill. + * + * @param mixed $settlement The Treasury bill's settlement date. + * The Treasury bill's settlement date is the date after the issue date when + * the Treasury bill is traded to the buyer. + * @param mixed $maturity The Treasury bill's maturity date. + * The maturity date is the date when the Treasury bill expires. + * @param int $price The Treasury bill's price per $100 face value + * + * @return float|string + */ + public static function yield($settlement, $maturity, $price) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $price = Functions::flattenSingleValue($price); + + if ( + is_string($maturity = DateTime::getDateValue($maturity)) || + is_string($settlement = DateTime::getDateValue($settlement)) + ) { + return Functions::VALUE(); + } + + // Validate + if (is_numeric($price)) { + if ($price <= 0) { + return Functions::NAN(); + } + + $daysBetweenSettlementAndMaturity = $maturity - $settlement; + + if ($daysBetweenSettlementAndMaturity > 360 || $daysBetweenSettlementAndMaturity < 0) { + return Functions::NAN(); + } + + return ((100 - $price) / $price) * (360 / $daysBetweenSettlementAndMaturity); + } + + return Functions::VALUE(); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/EffectTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/EffectTest.php index 7c866ed4..fd8bb36f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/EffectTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/EffectTest.php @@ -17,10 +17,12 @@ class EffectTest extends TestCase * @dataProvider providerEFFECT * * @param mixed $expectedResult + * @param mixed $rate + * @param mixed $periods */ - public function testEFFECT($expectedResult, ...$args): void + public function testEFFECT($expectedResult, $rate, $periods): void { - $result = Financial::EFFECT(...$args); + $result = Financial::EFFECT($rate, $periods); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/HelpersTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/HelpersTest.php new file mode 100644 index 00000000..d8a5d7d0 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/HelpersTest.php @@ -0,0 +1,27 @@ + [ '#NUM!', '25-Jan-2007', '15-Nov-2008', 3, 1, ], + 'Non-Numeric Frequency' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 'NaN', + 1, + ], + 'Invalid Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + -1, + ], + 'Non-Numeric Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + 'NaN', + ], + 'Same Date' => [ + '#NUM!', + '24-Dec-2000', + '24-Dec-2000', + 4, + 0, + ], + [ + 311, + '31-Jan-2021', + '20-Mar-2021', + 1, + 0, + ], + [ + 317, + '31-Jan-2021', + '20-Mar-2021', + 1, + 1, + ], + [ + 317, + '31-Jan-2020', + '20-Mar-2021', + 1, + 1, + ], + [ + 317, + '31-Jan-2021', + '20-Mar-2021', + 1, + 2, + ], + [ + 317, + '31-Jan-2021', + '20-Mar-2021', + 1, + 3, + ], + [ + 310, + '31-Jan-2021', + '20-Mar-2021', + 1, + 4, + ], + [ + 131, + '31-Jan-2021', + '20-Mar-2021', + 2, + 0, + ], + [ + 133, + '31-Jan-2021', + '20-Mar-2021', + 2, + 1, + ], + [ + 133, + '31-Jan-2020', + '20-Mar-2021', + 2, + 1, + ], + [ + 133, + '31-Jan-2021', + '20-Mar-2021', + 2, + 2, + ], + [ + 133, + '31-Jan-2021', + '20-Mar-2021', + 2, + 3, + ], + [ + 130, + '31-Jan-2021', + '20-Mar-2021', + 2, + 4, + ], + [ + 41, + '31-Jan-2021', + '20-Mar-2021', + 4, + 0, + ], + [ + 42, + '31-Jan-2021', + '20-Mar-2021', + 4, + 1, + ], + [ + 42, + '31-Jan-2020', + '20-Mar-2021', + 4, + 1, + ], + [ + 42, + '31-Jan-2021', + '20-Mar-2021', + 4, + 2, + ], + [ + 42, + '31-Jan-2021', + '20-Mar-2021', + 4, + 3, + ], + [ + 40, + '31-Jan-2021', + '20-Mar-2021', + 4, + 4, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPDAYS.php b/tests/data/Calculation/Financial/COUPDAYS.php index 384df0f1..acec49d9 100644 --- a/tests/data/Calculation/Financial/COUPDAYS.php +++ b/tests/data/Calculation/Financial/COUPDAYS.php @@ -51,11 +51,165 @@ return [ 2, 1, ], - [ + 'Invalid Frequency' => [ '#NUM!', '25-Jan-2007', '15-Nov-2008', 3, 1, ], + 'Non-Numeric Frequency' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 'NaN', + 1, + ], + 'Invalid Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + -1, + ], + 'Non-Numeric Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + 'NaN', + ], + 'Same Date' => [ + '#NUM!', + '24-Dec-2000', + '24-Dec-2000', + 4, + 0, + ], + [ + 360, + '31-Jan-2021', + '20-Mar-2021', + 1, + 0, + ], + [ + 365, + '31-Jan-2021', + '20-Mar-2021', + 1, + 1, + ], + [ + 366, + '31-Jan-2020', + '20-Mar-2021', + 1, + 1, + ], + [ + 360, + '31-Jan-2021', + '20-Mar-2021', + 1, + 2, + ], + [ + 365, + '31-Jan-2021', + '20-Mar-2021', + 1, + 3, + ], + [ + 360, + '31-Jan-2021', + '20-Mar-2021', + 1, + 4, + ], + [ + 180, + '31-Jan-2021', + '20-Mar-2021', + 2, + 0, + ], + [ + 181, + '31-Jan-2021', + '20-Mar-2021', + 2, + 1, + ], + [ + 182, + '31-Jan-2020', + '20-Mar-2021', + 2, + 1, + ], + [ + 180, + '31-Jan-2021', + '20-Mar-2021', + 2, + 2, + ], + [ + 182.5, + '31-Jan-2021', + '20-Mar-2021', + 2, + 3, + ], + [ + 180, + '31-Jan-2021', + '20-Mar-2021', + 2, + 4, + ], + [ + 90, + '31-Jan-2021', + '20-Mar-2021', + 4, + 0, + ], + [ + 90, + '31-Jan-2021', + '20-Mar-2021', + 4, + 1, + ], + [ + 91, + '31-Jan-2020', + '20-Mar-2021', + 4, + 1, + ], + [ + 90, + '31-Jan-2021', + '20-Mar-2021', + 4, + 2, + ], + [ + 91.25, + '31-Jan-2021', + '20-Mar-2021', + 4, + 3, + ], + [ + 90, + '31-Jan-2021', + '20-Mar-2021', + 4, + 4, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPDAYSNC.php b/tests/data/Calculation/Financial/COUPDAYSNC.php index 2b249cb8..87951dd1 100644 --- a/tests/data/Calculation/Financial/COUPDAYSNC.php +++ b/tests/data/Calculation/Financial/COUPDAYSNC.php @@ -30,11 +30,186 @@ return [ 2, 1, ], - [ + 'Invalid Frequency' => [ '#NUM!', '25-Jan-2007', '15-Nov-2008', 3, 1, ], + 'Non-Numeric Frequency' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 'NaN', + 1, + ], + 'Invalid Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + -1, + ], + 'Non-Numeric Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + 'NaN', + ], + 'Same Date' => [ + '#NUM!', + '24-Dec-2000', + '24-Dec-2000', + 4, + 0, + ], + [ + 49, + '31-Jan-2021', + '20-Mar-2021', + 1, + 0, + ], + [ + 49, + '01-Feb-2021', + '20-Mar-2021', + 1, + 0, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 1, + 1, + ], + [ + 49, + '31-Jan-2020', + '20-Mar-2021', + 1, + 1, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 1, + 2, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 1, + 3, + ], + [ + 50, + '31-Jan-2021', + '20-Mar-2021', + 1, + 4, + ], + [ + 49, + '31-Jan-2021', + '20-Mar-2021', + 2, + 0, + ], + [ + 49, + '01-Feb-2021', + '20-Mar-2021', + 2, + 0, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 2, + 1, + ], + [ + 49, + '31-Jan-2020', + '20-Mar-2021', + 2, + 1, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 2, + 2, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 2, + 3, + ], + [ + 50, + '31-Jan-2021', + '20-Mar-2021', + 2, + 4, + ], + [ + 49, + '31-Jan-2021', + '20-Mar-2021', + 4, + 0, + ], + [ + 49, + '01-Feb-2021', + '20-Mar-2021', + 4, + 0, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 4, + 1, + ], + [ + 49, + '31-Jan-2020', + '20-Mar-2021', + 4, + 1, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 4, + 2, + ], + [ + 48, + '31-Jan-2021', + '20-Mar-2021', + 4, + 3, + ], + [ + 50, + '31-Jan-2021', + '20-Mar-2021', + 4, + 4, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPNCD.php b/tests/data/Calculation/Financial/COUPNCD.php index eea6ad13..e3d452e5 100644 --- a/tests/data/Calculation/Financial/COUPNCD.php +++ b/tests/data/Calculation/Financial/COUPNCD.php @@ -30,14 +30,35 @@ return [ 2, 1, ], - [ + 'Invalid Frequency' => [ '#NUM!', '25-Jan-2007', '15-Nov-2008', 3, 1, ], - [ + 'Non-Numeric Frequency' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 'NaN', + 1, + ], + 'Invalid Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + -1, + ], + 'Non-Numeric Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + 'NaN', + ], + 'Same Date' => [ '#NUM!', '24-Dec-2000', '24-Dec-2000', @@ -65,4 +86,130 @@ return [ 4, 0, ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 1, + 0, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 1, + 1, + ], + [ + 43910, + '31-Jan-2020', + '20-Mar-2021', + 1, + 1, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 1, + 2, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 1, + 3, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 1, + 4, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 2, + 0, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 2, + 1, + ], + [ + 43910, + '31-Jan-2020', + '20-Mar-2021', + 2, + 1, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 2, + 2, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 2, + 3, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 2, + 4, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 4, + 0, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 4, + 1, + ], + [ + 43910, + '31-Jan-2020', + '20-Mar-2021', + 4, + 1, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 4, + 2, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 4, + 3, + ], + [ + 44275, + '31-Jan-2021', + '20-Mar-2021', + 4, + 4, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPNUM.php b/tests/data/Calculation/Financial/COUPNUM.php index 719ad733..b9ad73fa 100644 --- a/tests/data/Calculation/Financial/COUPNUM.php +++ b/tests/data/Calculation/Financial/COUPNUM.php @@ -31,13 +31,41 @@ return [ 2, 1, ], - [ + 'Invalid Frequency' => [ '#NUM!', '25-Jan-2007', '15-Nov-2008', 3, 1, ], + 'Non-Numeric Frequency' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 'NaN', + 1, + ], + 'Invalid Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + -1, + ], + 'Non-Numeric Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + 'NaN', + ], + 'Same Date' => [ + '#NUM!', + '24-Dec-2000', + '24-Dec-2000', + 4, + 0, + ], [ 5, '01-Jan-2008', @@ -108,4 +136,130 @@ return [ 2, 4, ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 1, + 0, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 1, + 1, + ], + [ + 2, + '31-Jan-2020', + '20-Mar-2021', + 1, + 1, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 1, + 2, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 1, + 3, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 1, + 4, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 2, + 0, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 2, + 1, + ], + [ + 3, + '31-Jan-2020', + '20-Mar-2021', + 2, + 1, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 2, + 2, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 2, + 3, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 2, + 4, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 4, + 0, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 4, + 1, + ], + [ + 5, + '31-Jan-2020', + '20-Mar-2021', + 4, + 1, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 4, + 2, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 4, + 3, + ], + [ + 1, + '31-Jan-2021', + '20-Mar-2021', + 4, + 4, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPPCD.php b/tests/data/Calculation/Financial/COUPPCD.php index 911637d6..6d2e2f22 100644 --- a/tests/data/Calculation/Financial/COUPPCD.php +++ b/tests/data/Calculation/Financial/COUPPCD.php @@ -30,14 +30,35 @@ return [ 2, 1, ], - [ + 'Invalid Frequency' => [ '#NUM!', '25-Jan-2007', '15-Nov-2008', 3, 1, ], - [ + 'Non-Numeric Frequency' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 'NaN', + 1, + ], + 'Invalid Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + -1, + ], + 'Non-Numeric Basis' => [ + '#NUM!', + '25-Jan-2007', + '15-Nov-2008', + 4, + 'NaN', + ], + 'Same Date' => [ '#NUM!', '24-Dec-2000', '24-Dec-2000', @@ -65,4 +86,130 @@ return [ 4, 0, ], + [ + 43910, + '31-Jan-2021', + '20-Mar-2021', + 1, + 0, + ], + [ + 43910, + '31-Jan-2021', + '20-Mar-2021', + 1, + 1, + ], + [ + 43544, + '31-Jan-2020', + '20-Mar-2021', + 1, + 1, + ], + [ + 43910, + '31-Jan-2021', + '20-Mar-2021', + 1, + 2, + ], + [ + 43910, + '31-Jan-2021', + '20-Mar-2021', + 1, + 3, + ], + [ + 43910, + '31-Jan-2021', + '20-Mar-2021', + 1, + 4, + ], + [ + 44094, + '31-Jan-2021', + '20-Mar-2021', + 2, + 0, + ], + [ + 44094, + '31-Jan-2021', + '20-Mar-2021', + 2, + 1, + ], + [ + 43728, + '31-Jan-2020', + '20-Mar-2021', + 2, + 1, + ], + [ + 44094, + '31-Jan-2021', + '20-Mar-2021', + 2, + 2, + ], + [ + 44094, + '31-Jan-2021', + '20-Mar-2021', + 2, + 3, + ], + [ + 44094, + '31-Jan-2021', + '20-Mar-2021', + 2, + 4, + ], + [ + 44185, + '31-Jan-2021', + '20-Mar-2021', + 4, + 0, + ], + [ + 44185, + '31-Jan-2021', + '20-Mar-2021', + 4, + 1, + ], + [ + 43819, + '31-Jan-2020', + '20-Mar-2021', + 4, + 1, + ], + [ + 44185, + '31-Jan-2021', + '20-Mar-2021', + 4, + 2, + ], + [ + 44185, + '31-Jan-2021', + '20-Mar-2021', + 4, + 3, + ], + [ + 44185, + '31-Jan-2021', + '20-Mar-2021', + 4, + 4, + ], ]; diff --git a/tests/data/Calculation/Financial/DaysPerYear.php b/tests/data/Calculation/Financial/DaysPerYear.php new file mode 100644 index 00000000..f9ca1c1d --- /dev/null +++ b/tests/data/Calculation/Financial/DaysPerYear.php @@ -0,0 +1,15 @@ + Date: Sat, 20 Mar 2021 22:52:04 +0100 Subject: [PATCH 55/89] First pass at extracting Financial Price functions for Securities (#1942) * Extracting Financial Price functions for Securities - PRICE(), PRICEMAT(), PRICEDISC() * Additional unit tests for PRICEDISC() invalid arguments * Additional unit tests for PRICEMAT() invalid arguments * Add docblock for PRICE() * Clarification on validation checks for <= 0 and < 0 --- .../Calculation/Calculation.php | 6 +- src/PhpSpreadsheet/Calculation/Financial.php | 183 +++------- .../Calculation/Financial/Coupons.php | 2 +- .../Calculation/Financial/Securities.php | 321 ++++++++++++++++++ .../data/Calculation/Financial/PRICEDISC.php | 32 ++ tests/data/Calculation/Financial/PRICEMAT.php | 28 ++ 6 files changed, 426 insertions(+), 146 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Securities.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 05fe8a81..36f5efe4 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1983,17 +1983,17 @@ class Calculation ], 'PRICE' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PRICE'], + 'functionCall' => [Financial\Securities::class, 'price'], 'argumentCount' => '6,7', ], 'PRICEDISC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PRICEDISC'], + 'functionCall' => [Financial\Securities::class, 'discounted'], 'argumentCount' => '4,5', ], 'PRICEMAT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PRICEMAT'], + 'functionCall' => [Financial\Securities::class, 'maturity'], 'argumentCount' => '5,6', ], 'PROB' => [ diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index f6f5c775..9735e9f4 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -11,15 +11,6 @@ class Financial const FINANCIAL_PRECISION = 1.0e-08; - private static function isValidFrequency($frequency) - { - if (($frequency == 1) || ($frequency == 2) || ($frequency == 4)) { - return true; - } - - return false; - } - private static function interestAndPrincipal($rate = 0, $per = 0, $nper = 0, $pv = 0, $fv = 0, $type = 0) { $pmt = self::PMT($rate, $nper, $pv, $fv, $type); @@ -1370,79 +1361,39 @@ class Financial return $interestAndPrincipal[1]; } - private static function validatePrice($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis) - { - if (is_string($settlement)) { - return Functions::VALUE(); - } - if (is_string($maturity)) { - return Functions::VALUE(); - } - if (!is_numeric($rate)) { - return Functions::VALUE(); - } - if (!is_numeric($yield)) { - return Functions::VALUE(); - } - if (!is_numeric($redemption)) { - return Functions::VALUE(); - } - if (!is_numeric($frequency)) { - return Functions::VALUE(); - } - if (!is_numeric($basis)) { - return Functions::VALUE(); - } - - return ''; - } - + /** + * PRICE. + * + * Returns the price per $100 face value of a security that pays periodic interest. + * + * @Deprecated 1.18.0 + * + * @see Use the price() method in the Financial\Securities class instead + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue date when the security + * is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param float $rate the security's annual coupon rate + * @param float $yield the security's annual yield + * @param float $redemption The number of coupon payments per year. + * For annual payments, frequency = 1; + * for semiannual, frequency = 2; + * for quarterly, frequency = 4. + * @param int $frequency + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ public static function PRICE($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $rate = Functions::flattenSingleValue($rate); - $yield = Functions::flattenSingleValue($yield); - $redemption = Functions::flattenSingleValue($redemption); - $frequency = Functions::flattenSingleValue($frequency); - $basis = Functions::flattenSingleValue($basis); - - $settlement = DateTime::getDateValue($settlement); - $maturity = DateTime::getDateValue($maturity); - $rslt = self::validatePrice($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis); - if ($rslt) { - return $rslt; - } - $rate = (float) $rate; - $yield = (float) $yield; - $redemption = (float) $redemption; - $frequency = (int) $frequency; - $basis = (int) $basis; - - if ( - ($settlement > $maturity) || - (!self::isValidFrequency($frequency)) || - (($basis < 0) || ($basis > 4)) - ) { - return Functions::NAN(); - } - - $dsc = self::COUPDAYSNC($settlement, $maturity, $frequency, $basis); - $e = self::COUPDAYS($settlement, $maturity, $frequency, $basis); - $n = self::COUPNUM($settlement, $maturity, $frequency, $basis); - $a = self::COUPDAYBS($settlement, $maturity, $frequency, $basis); - - $baseYF = 1.0 + ($yield / $frequency); - $rfp = 100 * ($rate / $frequency); - $de = $dsc / $e; - - $result = $redemption / $baseYF ** (--$n + $de); - for ($k = 0; $k <= $n; ++$k) { - $result += $rfp / ($baseYF ** ($k + $de)); - } - $result -= $rfp * ($a / $e); - - return $result; + return Financial\Securities::price($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis); } /** @@ -1450,6 +1401,10 @@ class Financial * * Returns the price per $100 face value of a discounted security. * + * @Deprecated 1.18.0 + * + * @see Use the discounted() method in the Financial\Securities class instead + * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. @@ -1467,27 +1422,7 @@ class Financial */ public static function PRICEDISC($settlement, $maturity, $discount, $redemption, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $discount = (float) Functions::flattenSingleValue($discount); - $redemption = (float) Functions::flattenSingleValue($redemption); - $basis = (int) Functions::flattenSingleValue($basis); - - // Validate - if ((is_numeric($discount)) && (is_numeric($redemption)) && (is_numeric($basis))) { - if (($discount <= 0) || ($redemption <= 0)) { - return Functions::NAN(); - } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - - return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity); - } - - return Functions::VALUE(); + return Financial\Securities::discounted($settlement, $maturity, $discount, $redemption, $basis); } /** @@ -1495,6 +1430,10 @@ class Financial * * Returns the price per $100 face value of a security that pays interest at maturity. * + * @Deprecated 1.18.0 + * + * @see Use the maturity() method in the Financial\Securities class instead + * * @param mixed $settlement The security's settlement date. * The security's settlement date is the date after the issue date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. @@ -1513,47 +1452,7 @@ class Financial */ public static function PRICEMAT($settlement, $maturity, $issue, $rate, $yield, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $issue = Functions::flattenSingleValue($issue); - $rate = Functions::flattenSingleValue($rate); - $yield = Functions::flattenSingleValue($yield); - $basis = (int) Functions::flattenSingleValue($basis); - - // Validate - if (is_numeric($rate) && is_numeric($yield)) { - if (($rate <= 0) || ($yield <= 0)) { - return Functions::NAN(); - } - $daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); - if (!is_numeric($daysPerYear)) { - return $daysPerYear; - } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); - if (!is_numeric($daysBetweenIssueAndSettlement)) { - // return date error - return $daysBetweenIssueAndSettlement; - } - $daysBetweenIssueAndSettlement *= $daysPerYear; - $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); - if (!is_numeric($daysBetweenIssueAndMaturity)) { - // return date error - return $daysBetweenIssueAndMaturity; - } - $daysBetweenIssueAndMaturity *= $daysPerYear; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - $daysBetweenSettlementAndMaturity *= $daysPerYear; - - return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) / - (1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) - - (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100); - } - - return Functions::VALUE(); + return Financial\Securities::maturity($settlement, $maturity, $issue, $rate, $yield, $basis); } /** diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index ff99c839..835ef633 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -419,7 +419,7 @@ class Coupons return $frequency; } - private static function validateBasis($basis) + private static function validateBasis($basis): int { if (!is_numeric($basis)) { throw new Exception(Functions::NAN()); diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities.php b/src/PhpSpreadsheet/Calculation/Financial/Securities.php new file mode 100644 index 00000000..761fc18a --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities.php @@ -0,0 +1,321 @@ +getMessage(); + } + + $dsc = Coupons::COUPDAYSNC($settlement, $maturity, $frequency, $basis); + $e = Coupons::COUPDAYS($settlement, $maturity, $frequency, $basis); + $n = Coupons::COUPNUM($settlement, $maturity, $frequency, $basis); + $a = Coupons::COUPDAYBS($settlement, $maturity, $frequency, $basis); + + $baseYF = 1.0 + ($yield / $frequency); + $rfp = 100 * ($rate / $frequency); + $de = $dsc / $e; + + $result = $redemption / $baseYF ** (--$n + $de); + for ($k = 0; $k <= $n; ++$k) { + $result += $rfp / ($baseYF ** ($k + $de)); + } + $result -= $rfp * ($a / $e); + + return $result; + } + + /** + * PRICEDISC. + * + * Returns the price per $100 face value of a discounted security. + * + * @param mixed $settlement The security's settlement date. + * The security settlement date is the date after the issue date when the security + * is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param float $discount The security's discount rate + * @param float $redemption The security's redemption value per $100 face value + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function discounted($settlement, $maturity, $discount, $redemption, $basis = 0) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $discount = Functions::flattenSingleValue($discount); + $redemption = Functions::flattenSingleValue($redemption); + $basis = Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateSecurityPeriod($settlement, $maturity); + $discount = self::validateDiscount($discount); + $redemption = self::validateRedemption($redemption); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return $daysBetweenSettlementAndMaturity; + } + + return $redemption * (1 - $discount * $daysBetweenSettlementAndMaturity); + } + + /** + * PRICEMAT. + * + * Returns the price per $100 face value of a security that pays interest at maturity. + * + * @param mixed $settlement The security's settlement date. + * The security's settlement date is the date after the issue date when the + * security is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $issue The security's issue date + * @param float $rate The security's interest rate at date of issue + * @param float $yield The security's annual yield + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function maturity($settlement, $maturity, $issue, $rate, $yield, $basis = 0) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $issue = Functions::flattenSingleValue($issue); + $rate = Functions::flattenSingleValue($rate); + $yield = Functions::flattenSingleValue($yield); + $basis = Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateSecurityPeriod($settlement, $maturity); + $issue = self::validateIssueDate($issue); + $rate = self::validateRate($rate); + $yield = self::validateYield($yield); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + if (!is_numeric($daysPerYear)) { + return $daysPerYear; + } + $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return $daysBetweenIssueAndSettlement; + } + $daysBetweenIssueAndSettlement *= $daysPerYear; + $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); + if (!is_numeric($daysBetweenIssueAndMaturity)) { + // return date error + return $daysBetweenIssueAndMaturity; + } + $daysBetweenIssueAndMaturity *= $daysPerYear; + $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return $daysBetweenSettlementAndMaturity; + } + $daysBetweenSettlementAndMaturity *= $daysPerYear; + + return (100 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate * 100)) / + (1 + (($daysBetweenSettlementAndMaturity / $daysPerYear) * $yield)) - + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate * 100); + } + + private static function validateInputDate($date) + { + $date = DateTime::getDateValue($date); + if (is_string($date)) { + throw new Exception(Functions::VALUE()); + } + + return $date; + } + + private static function validateSettlementDate($settlement) + { + return self::validateInputDate($settlement); + } + + private static function validateMaturityDate($maturity) + { + return self::validateInputDate($maturity); + } + + private static function validateIssueDate($issue) + { + return self::validateInputDate($issue); + } + + private static function validateSecurityPeriod($settlement, $maturity): void + { + if ($settlement >= $maturity) { + throw new Exception(Functions::NAN()); + } + } + + private static function validateRate($rate): float + { + if (!is_numeric($rate)) { + throw new Exception(Functions::VALUE()); + } + + $rate = (float) $rate; + if ($rate < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $rate; + } + + private static function validateYield($yield): float + { + if (!is_numeric($yield)) { + throw new Exception(Functions::VALUE()); + } + + $yield = (float) $yield; + if ($yield < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $yield; + } + + private static function validateRedemption($redemption): float + { + if (!is_numeric($redemption)) { + throw new Exception(Functions::VALUE()); + } + + $redemption = (float) $redemption; + if ($redemption <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $redemption; + } + + private static function validateDiscount($discount): float + { + if (!is_numeric($discount)) { + throw new Exception(Functions::VALUE()); + } + + $discount = (float) $discount; + if ($discount <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $discount; + } + + private static function validateFrequency($frequency): int + { + if (!is_numeric($frequency)) { + throw new Exception(Functions::VALUE()); + } + + $frequency = (int) $frequency; + if ( + ($frequency !== self::FREQUENCY_ANNUAL) && + ($frequency !== self::FREQUENCY_SEMI_ANNUAL) && + ($frequency !== self::FREQUENCY_QUARTERLY) + ) { + throw new Exception(Functions::NAN()); + } + + return $frequency; + } + + private static function validateBasis($basis): int + { + if (!is_numeric($basis)) { + throw new Exception(Functions::VALUE()); + } + + $basis = (int) $basis; + if (($basis < 0) || ($basis > 4)) { + throw new Exception(Functions::NAN()); + } + + return $basis; + } +} diff --git a/tests/data/Calculation/Financial/PRICEDISC.php b/tests/data/Calculation/Financial/PRICEDISC.php index 88a0d94e..9413ca39 100644 --- a/tests/data/Calculation/Financial/PRICEDISC.php +++ b/tests/data/Calculation/Financial/PRICEDISC.php @@ -9,4 +9,36 @@ return [ 97.6311475409836, ['2008-02-15', '2008-11-30', 0.03, 100, 1], ], + [ + '#VALUE!', + ['Invalid Date', '2008-11-30', 0.03, 100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', 'Invalid Date', 0.03, 100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', '2008-11-30', 'NaN', 100, 1], + ], + [ + '#NUM!', + ['2008-02-15', '2008-11-30', -0.03, 100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', '2008-11-30', 0.03, 'NaN', 1], + ], + [ + '#NUM!', + ['2008-02-15', '2008-11-30', 0.03, -100, 1], + ], + [ + '#VALUE!', + ['2008-02-15', '2008-11-30', 0.03, 100, 'NaN'], + ], + [ + '#NUM!', + ['2008-02-15', '2008-11-30', 0.03, 100, -1], + ], ]; diff --git a/tests/data/Calculation/Financial/PRICEMAT.php b/tests/data/Calculation/Financial/PRICEMAT.php index ac5c242e..2de50927 100644 --- a/tests/data/Calculation/Financial/PRICEMAT.php +++ b/tests/data/Calculation/Financial/PRICEMAT.php @@ -13,4 +13,32 @@ return [ 93.0909090909091, '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, 0.08, ], + [ + '#VALUE!', + 'Invalid Date', '1-Jan-2027', '1-Jan-2017', 0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', 'Invalid Date', '1-Jan-2017', 0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', '1-Jan-2027', 'Invalid Date', 0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 'NaN', 0.08, + ], + [ + '#NUM!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', -0.07, 0.08, + ], + [ + '#VALUE!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, 'NaN', + ], + [ + '#NUM!', + '1-Jul-2017', '1-Jan-2027', '1-Jan-2017', 0.07, -0.08, + ], ]; From 9beacd21be4725bc2a73aebd3b74df75b7658d09 Mon Sep 17 00:00:00 2001 From: oleibman Date: Sun, 21 Mar 2021 01:12:05 -0700 Subject: [PATCH 56/89] Complete Breakup Of Calculation/DateTime Functions (#1937) * Complete Breakup Of Calculation/DateTime Functions In conjunction with parallel breakups happening in other areas of Calculation, this change breaks up all the DateTime functions into their own classes. All methods remaining in DateTime itself have a doc block deprecation notice, and consist only of stub code to call the replacement methods. Coverage of DateTime itself and all the replacement methods is 100%. There is only one substantive change to the code (see next paragraph). Among the non-substantive changes, it now adopts the same parsing technique (throwing and catching exceptions) already in use in Engineering and MathTrig. Boolean parameters are allowed in lieu of numbers when Excel allows them. Most of the code changes involve refactoring due to the need to avoid Scrutinizer "complexity" failures in what it will consider to be new methods. Issue #1936 was opened just as I was staging this. It is now fixed. One existing WORKDAY test was wrong (noted in a comment in the test data file), and a bunch of new tests are added. I found it confusing to use DateTime as a node of the the class name since most of the methods invoke native DateTime methods. So, everything is moved to directory DateTimeExcel, and that is what is used in the class names. There are several follow-up activities that I am planning to undertake if this PR is merged. - ODS supports dates well before 1900. There are exactly 2 assertions for this functionality. More are needed (and some functions might have to change to accept this). - WEEKDAY has some poorly documented extra options for "style" which are not yet implemented. - Most tests have been changed to use a formula as entered on a spreadsheet rather than a direct call to the method which implements the formula. There are 3 exceptions at this time. WORKDAY and NETWORKDAYS, which include arrays as part of their parameters, are more complicated than most. YEARFRAC was just too large to deal with now. - There are direct calls to the now-deprecated methods in both source code and tests, mostly in Financial code, but possibly in others as well. These need to be changed. - Some constants, none "officially" documented, remain in the original class. These should be either deleted or marked deprecated. I wasn't sure if deprecation was even possible (or desirable), and did not want that to be something which would cause Scrutinizer to fail the change. * Deprecate Now-unused Constants, Fix Yearfrac bug, Change 3 Tests Add new DateTime/Constants class, initially populated with constants used in Weeknum. MS has another inconsistency with how it handles null cells in Yearfrac. Change PhpSpreadsheet to behave compatibly with this bug. I have modified YearFrac, WorkDay, and NetworkDays tests to be more to my liking. Many tests added to YearFrac because of the bug above. Only minor modifications to the existing tests for the others. --- .../Calculation/Calculation.php | 46 +- src/PhpSpreadsheet/Calculation/DateTime.php | 1291 +++-------------- .../Calculation/DateTimeExcel/Constants.php | 37 + .../Calculation/DateTimeExcel/DateDif.php | 146 ++ .../Calculation/DateTimeExcel/DateValue.php | 151 ++ .../Calculation/DateTimeExcel/Datefunc.php | 168 +++ .../Calculation/DateTimeExcel/Day.php | 61 + .../Calculation/DateTimeExcel/Days.php | 51 + .../Calculation/DateTimeExcel/Days360.php | 106 ++ .../Calculation/DateTimeExcel/EDate.php | 45 + .../Calculation/DateTimeExcel/EoMonth.php | 47 + .../Calculation/DateTimeExcel/Helpers.php | 291 ++++ .../Calculation/DateTimeExcel/Hour.php | 44 + .../Calculation/DateTimeExcel/IsoWeekNum.php | 55 + .../Calculation/DateTimeExcel/Minute.php | 44 + .../Calculation/DateTimeExcel/Month.php | 40 + .../Calculation/DateTimeExcel/NetworkDays.php | 102 ++ .../Calculation/DateTimeExcel/Now.php | 34 + .../Calculation/DateTimeExcel/Second.php | 44 + .../Calculation/DateTimeExcel/Time.php | 116 ++ .../Calculation/DateTimeExcel/TimeValue.php | 61 + .../Calculation/DateTimeExcel/Today.php | 34 + .../Calculation/DateTimeExcel/WeekDay.php | 80 + .../Calculation/DateTimeExcel/WeekNum.php | 130 ++ .../Calculation/DateTimeExcel/WorkDay.php | 182 +++ .../Calculation/DateTimeExcel/Year.php | 40 + .../Calculation/DateTimeExcel/YearFrac.php | 120 ++ .../Functions/DateTime/AllSetupTeardown.php | 71 + .../Functions/DateTime/DateDifTest.php | 26 +- .../Functions/DateTime/DateTest.php | 56 +- .../Functions/DateTime/DateValueTest.php | 53 +- .../Functions/DateTime/DayTest.php | 65 +- .../Functions/DateTime/Days360Test.php | 27 +- .../Functions/DateTime/DaysTest.php | 43 +- .../Functions/DateTime/EDateTest.php | 33 +- .../Functions/DateTime/EoMonthTest.php | 36 +- .../Functions/DateTime/HourTest.php | 24 +- .../Functions/DateTime/IsoWeekNumTest.php | 44 +- .../Functions/DateTime/MinuteTest.php | 24 +- .../Functions/DateTime/MonthTest.php | 24 +- .../Functions/DateTime/MovedFunctionsTest.php | 63 + .../Functions/DateTime/NetworkDaysTest.php | 50 +- .../Functions/DateTime/NowTest.php | 8 +- .../Functions/DateTime/SecondTest.php | 24 +- .../Functions/DateTime/TimeTest.php | 52 +- .../Functions/DateTime/TimeValueTest.php | 28 +- .../Functions/DateTime/TodayTest.php | 34 + .../Functions/DateTime/WeekDayTest.php | 35 +- .../Functions/DateTime/WeekNumTest.php | 51 +- .../Functions/DateTime/WorkDayTest.php | 50 +- .../Functions/DateTime/YearFracTest.php | 42 +- .../Functions/DateTime/YearTest.php | 24 +- tests/data/Calculation/DateTime/DATE.php | 393 +---- tests/data/Calculation/DateTime/DATEDIF.php | 528 ++----- tests/data/Calculation/DateTime/DATEVALUE.php | 379 +---- tests/data/Calculation/DateTime/DAY.php | 81 +- .../Calculation/DateTime/DAYOpenOffice.php | 19 + tests/data/Calculation/DateTime/DAYS.php | 97 +- tests/data/Calculation/DateTime/DAYS360.php | 174 +-- tests/data/Calculation/DateTime/EDATE.php | 80 +- tests/data/Calculation/DateTime/EOMONTH.php | 92 +- tests/data/Calculation/DateTime/HOUR.php | 65 +- .../data/Calculation/DateTime/ISOWEEKNUM.php | 69 +- .../Calculation/DateTime/ISOWEEKNUM1904.php | 33 + tests/data/Calculation/DateTime/MINUTE.php | 65 +- tests/data/Calculation/DateTime/MONTH.php | 68 +- .../data/Calculation/DateTime/NETWORKDAYS.php | 19 +- tests/data/Calculation/DateTime/SECOND.php | 65 +- tests/data/Calculation/DateTime/TIME.php | 112 +- tests/data/Calculation/DateTime/TIMEVALUE.php | 70 +- tests/data/Calculation/DateTime/WEEKDAY.php | 156 +- tests/data/Calculation/DateTime/WEEKNUM.php | 275 +--- .../data/Calculation/DateTime/WEEKNUM1904.php | 92 ++ tests/data/Calculation/DateTime/WORKDAY.php | 28 +- tests/data/Calculation/DateTime/YEAR.php | 62 +- tests/data/Calculation/DateTime/YEARFRAC.php | 50 + 76 files changed, 3874 insertions(+), 3751 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Constants.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Datefunc.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Day.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/EDate.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/EoMonth.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Hour.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/IsoWeekNum.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Minute.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Now.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Second.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Today.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekDay.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/Year.php create mode 100644 src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MovedFunctionsTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TodayTest.php create mode 100644 tests/data/Calculation/DateTime/DAYOpenOffice.php create mode 100644 tests/data/Calculation/DateTime/ISOWEEKNUM1904.php create mode 100644 tests/data/Calculation/DateTime/WEEKNUM1904.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 36f5efe4..dbea0850 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -755,17 +755,17 @@ class Calculation ], 'DATE' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DATE'], + 'functionCall' => [DateTimeExcel\Datefunc::class, 'funcDate'], 'argumentCount' => '3', ], 'DATEDIF' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DATEDIF'], + 'functionCall' => [DateTimeExcel\DateDif::class, 'funcDateDif'], 'argumentCount' => '2,3', ], 'DATEVALUE' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DATEVALUE'], + 'functionCall' => [DateTimeExcel\DateValue::class, 'funcDateValue'], 'argumentCount' => '1', ], 'DAVERAGE' => [ @@ -775,17 +775,17 @@ class Calculation ], 'DAY' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DAYOFMONTH'], + 'functionCall' => [DateTimeExcel\Day::class, 'funcDay'], 'argumentCount' => '1', ], 'DAYS' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DAYS'], + 'functionCall' => [DateTimeExcel\Days::class, 'funcDays'], 'argumentCount' => '2', ], 'DAYS360' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DAYS360'], + 'functionCall' => [DateTimeExcel\Days360::class, 'funcDays360'], 'argumentCount' => '2,3', ], 'DB' => [ @@ -920,7 +920,7 @@ class Calculation ], 'EDATE' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'EDATE'], + 'functionCall' => [DateTimeExcel\EDate::class, 'funcEDate'], 'argumentCount' => '2', ], 'EFFECT' => [ @@ -935,7 +935,7 @@ class Calculation ], 'EOMONTH' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'EOMONTH'], + 'functionCall' => [DateTimeExcel\EoMonth::class, 'funcEoMonth'], 'argumentCount' => '2', ], 'ERF' => [ @@ -1237,7 +1237,7 @@ class Calculation ], 'HOUR' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'HOUROFDAY'], + 'functionCall' => [DateTimeExcel\Hour::class, 'funcHour'], 'argumentCount' => '1', ], 'HYPERLINK' => [ @@ -1501,7 +1501,7 @@ class Calculation ], 'ISOWEEKNUM' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'ISOWEEKNUM'], + 'functionCall' => [DateTimeExcel\IsoWeekNum::class, 'funcIsoWeekNum'], 'argumentCount' => '1', ], 'ISPMT' => [ @@ -1681,7 +1681,7 @@ class Calculation ], 'MINUTE' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'MINUTE'], + 'functionCall' => [DateTimeExcel\Minute::class, 'funcMinute'], 'argumentCount' => '1', ], 'MINVERSE' => [ @@ -1721,7 +1721,7 @@ class Calculation ], 'MONTH' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'MONTHOFYEAR'], + 'functionCall' => [DateTimeExcel\Month::class, 'funcMonth'], 'argumentCount' => '1', ], 'MROUND' => [ @@ -1761,7 +1761,7 @@ class Calculation ], 'NETWORKDAYS' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'NETWORKDAYS'], + 'functionCall' => [DateTimeExcel\NetworkDays::class, 'funcNetworkDays'], 'argumentCount' => '2-3', ], 'NETWORKDAYS.INTL' => [ @@ -1821,7 +1821,7 @@ class Calculation ], 'NOW' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DATETIMENOW'], + 'functionCall' => [DateTimeExcel\Now::class, 'funcNow'], 'argumentCount' => '0', ], 'NPER' => [ @@ -2175,7 +2175,7 @@ class Calculation ], 'SECOND' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'SECOND'], + 'functionCall' => [DateTimeExcel\Second::class, 'funcSecond'], 'argumentCount' => '1', ], 'SEQUENCE' => [ @@ -2421,12 +2421,12 @@ class Calculation ], 'TIME' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'TIME'], + 'functionCall' => [DateTimeExcel\Time::class, 'funcTime'], 'argumentCount' => '3', ], 'TIMEVALUE' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'TIMEVALUE'], + 'functionCall' => [DateTimeExcel\TimeValue::class, 'funcTimeValue'], 'argumentCount' => '1', ], 'TINV' => [ @@ -2446,7 +2446,7 @@ class Calculation ], 'TODAY' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'DATENOW'], + 'functionCall' => [DateTimeExcel\Today::class, 'funcToday'], 'argumentCount' => '0', ], 'TRANSPOSE' => [ @@ -2571,12 +2571,12 @@ class Calculation ], 'WEEKDAY' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'WEEKDAY'], + 'functionCall' => [DateTimeExcel\WeekDay::class, 'funcWeekDay'], 'argumentCount' => '1,2', ], 'WEEKNUM' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'WEEKNUM'], + 'functionCall' => [DateTimeExcel\WeekNum::class, 'funcWeekNum'], 'argumentCount' => '1,2', ], 'WEIBULL' => [ @@ -2591,7 +2591,7 @@ class Calculation ], 'WORKDAY' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'WORKDAY'], + 'functionCall' => [DateTimeExcel\WorkDay::class, 'funcWorkDay'], 'argumentCount' => '2-3', ], 'WORKDAY.INTL' => [ @@ -2626,12 +2626,12 @@ class Calculation ], 'YEAR' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'YEAR'], + 'functionCall' => [DateTimeExcel\Year::class, 'funcYear'], 'argumentCount' => '1', ], 'YEARFRAC' => [ 'category' => Category::CATEGORY_DATE_AND_TIME, - 'functionCall' => [DateTime::class, 'YEARFRAC'], + 'functionCall' => [DateTimeExcel\YearFrac::class, 'funcYearFrac'], 'argumentCount' => '2,3', ], 'YIELD' => [ diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index 64d72c2b..744d9589 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -2,127 +2,36 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; -use DateTimeImmutable; use DateTimeInterface; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; class DateTime { /** * Identify if a year is a leap year or not. * + * @Deprecated 2.0.0 Use the method isLeapYear in the DateTimeExcel\Helpers class instead + * * @param int|string $year The year to test * * @return bool TRUE if the year is a leap year, otherwise FALSE */ public static function isLeapYear($year) { - return (($year % 4) === 0) && (($year % 100) !== 0) || (($year % 400) === 0); - } - - /** - * Return the number of days between two dates based on a 360 day calendar. - * - * @param int $startDay Day of month of the start date - * @param int $startMonth Month of the start date - * @param int $startYear Year of the start date - * @param int $endDay Day of month of the start date - * @param int $endMonth Month of the start date - * @param int $endYear Year of the start date - * @param bool $methodUS Whether to use the US method or the European method of calculation - * - * @return int Number of days between the start date and the end date - */ - private static function dateDiff360($startDay, $startMonth, $startYear, $endDay, $endMonth, $endYear, $methodUS) - { - if ($startDay == 31) { - --$startDay; - } elseif ($methodUS && ($startMonth == 2 && ($startDay == 29 || ($startDay == 28 && !self::isLeapYear($startYear))))) { - $startDay = 30; - } - if ($endDay == 31) { - if ($methodUS && $startDay != 30) { - $endDay = 1; - if ($endMonth == 12) { - ++$endYear; - $endMonth = 1; - } else { - ++$endMonth; - } - } else { - $endDay = 30; - } - } - - return $endDay + $endMonth * 30 + $endYear * 360 - $startDay - $startMonth * 30 - $startYear * 360; + return DateTimeExcel\Helpers::isLeapYear($year); } /** * getDateValue. * + * @Deprecated 2.0.0 Use the method getDateValueNoThrow in the DateTimeExcel\Helpers class instead + * * @param mixed $dateValue * * @return mixed Excel date/time serial value, or string if error */ public static function getDateValue($dateValue) { - if (!is_numeric($dateValue)) { - if ((is_object($dateValue)) && ($dateValue instanceof DateTimeInterface)) { - $dateValue = Date::PHPToExcel($dateValue); - } else { - $saveReturnDateType = Functions::getReturnDateType(); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - $dateValue = self::DATEVALUE($dateValue); - Functions::setReturnDateType($saveReturnDateType); - } - } - - return $dateValue; - } - - /** - * getTimeValue. - * - * @param string $timeValue - * - * @return mixed Excel date/time serial value, or string if error - */ - private static function getTimeValue($timeValue) - { - $saveReturnDateType = Functions::getReturnDateType(); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - $timeValue = self::TIMEVALUE($timeValue); - Functions::setReturnDateType($saveReturnDateType); - - return $timeValue; - } - - private static function adjustDateByMonths($dateValue = 0, $adjustmentMonths = 0) - { - // Execute function - $PHPDateObject = Date::excelToDateTimeObject($dateValue); - $oMonth = (int) $PHPDateObject->format('m'); - $oYear = (int) $PHPDateObject->format('Y'); - - $adjustmentMonthsString = (string) $adjustmentMonths; - if ($adjustmentMonths > 0) { - $adjustmentMonthsString = '+' . $adjustmentMonths; - } - if ($adjustmentMonths != 0) { - $PHPDateObject->modify($adjustmentMonthsString . ' months'); - } - $nMonth = (int) $PHPDateObject->format('m'); - $nYear = (int) $PHPDateObject->format('Y'); - - $monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12); - if ($monthDiff != $adjustmentMonths) { - $adjustDays = (int) $PHPDateObject->format('d'); - $adjustDaysString = '-' . $adjustDays . ' days'; - $PHPDateObject->modify($adjustDaysString); - } - - return $PHPDateObject; + return DateTimeExcel\Helpers::getDateValueNoThrow($dateValue); } /** @@ -136,6 +45,8 @@ class DateTime * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date * and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. * + * @Deprecated 2.0.0 Use the funcNow method in the DateTimeExcel\Now class instead + * * Excel Function: * NOW() * @@ -144,10 +55,7 @@ class DateTime */ public static function DATETIMENOW() { - $dti = new DateTimeImmutable(); - $dateArray = date_parse($dti->format('c')); - - return is_array($dateArray) ? self::returnIn3FormatsArray($dateArray) : Functions::VALUE(); + return DateTimeExcel\Now::funcNow(); } /** @@ -161,6 +69,8 @@ class DateTime * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date * and time format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. * + * @Deprecated 2.0.0 Use the funcToday method in the DateTimeExcel\Today class instead + * * Excel Function: * TODAY() * @@ -169,10 +79,7 @@ class DateTime */ public static function DATENOW() { - $dti = new DateTimeImmutable(); - $dateArray = date_parse($dti->format('c')); - - return is_array($dateArray) ? self::returnIn3FormatsArray($dateArray, true) : Functions::VALUE(); + return DateTimeExcel\Today::funcToday(); } /** @@ -183,6 +90,8 @@ class DateTime * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. * + * @Deprecated 2.0.0 Use the funcDate method in the DateTimeExcel\Date class instead + * * Excel Function: * DATE(year,month,day) * @@ -226,65 +135,7 @@ class DateTime */ public static function DATE($year = 0, $month = 1, $day = 1) { - $year = Functions::flattenSingleValue($year); - $month = Functions::flattenSingleValue($month); - $day = Functions::flattenSingleValue($day); - - if (($month !== null) && (!is_numeric($month))) { - $month = Date::monthStringToNumber($month); - } - - if (($day !== null) && (!is_numeric($day))) { - $day = Date::dayStringToNumber($day); - } - - $year = ($year !== null) ? StringHelper::testStringAsNumeric($year) : 0; - $month = ($month !== null) ? StringHelper::testStringAsNumeric($month) : 0; - $day = ($day !== null) ? StringHelper::testStringAsNumeric($day) : 0; - if ( - (!is_numeric($year)) || - (!is_numeric($month)) || - (!is_numeric($day)) - ) { - return Functions::VALUE(); - } - $year = (int) $year; - $month = (int) $month; - $day = (int) $day; - - $baseYear = Date::getExcelCalendar(); - // Validate parameters - if ($year < ($baseYear - 1900)) { - return Functions::NAN(); - } - if ((($baseYear - 1900) != 0) && ($year < $baseYear) && ($year >= 1900)) { - return Functions::NAN(); - } - - if (($year < $baseYear) && ($year >= ($baseYear - 1900))) { - $year += 1900; - } - - if ($month < 1) { - // Handle year/month adjustment if month < 1 - --$month; - $year += ceil($month / 12) - 1; - $month = 13 - abs($month % 12); - } elseif ($month > 12) { - // Handle year/month adjustment if month > 12 - $year += floor($month / 12); - $month = ($month % 12); - } - - // Re-validate the year parameter after adjustments - if (($year < $baseYear) || ($year >= 10000)) { - return Functions::NAN(); - } - - // Execute function - $excelDateValue = Date::formattedPHPToExcel($year, $month, $day); - - return self::returnIn3FormatsFloat($excelDateValue); + return DateTimeExcel\Datefunc::funcDate($year, $month, $day); } /** @@ -295,6 +146,8 @@ class DateTime * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. * + * @Deprecated 2.0.0 Use the funcTime method in the DateTimeExcel\Time class instead + * * Excel Function: * TIME(hour,minute,second) * @@ -315,73 +168,7 @@ class DateTime */ public static function TIME($hour = 0, $minute = 0, $second = 0) { - $hour = Functions::flattenSingleValue($hour); - $minute = Functions::flattenSingleValue($minute); - $second = Functions::flattenSingleValue($second); - - if ($hour == '') { - $hour = 0; - } - if ($minute == '') { - $minute = 0; - } - if ($second == '') { - $second = 0; - } - - if ((!is_numeric($hour)) || (!is_numeric($minute)) || (!is_numeric($second))) { - return Functions::VALUE(); - } - $hour = (int) $hour; - $minute = (int) $minute; - $second = (int) $second; - - if ($second < 0) { - $minute += floor($second / 60); - $second = 60 - abs($second % 60); - if ($second == 60) { - $second = 0; - } - } elseif ($second >= 60) { - $minute += floor($second / 60); - $second = $second % 60; - } - if ($minute < 0) { - $hour += floor($minute / 60); - $minute = 60 - abs($minute % 60); - if ($minute == 60) { - $minute = 0; - } - } elseif ($minute >= 60) { - $hour += floor($minute / 60); - $minute = $minute % 60; - } - - if ($hour > 23) { - $hour = $hour % 24; - } elseif ($hour < 0) { - return Functions::NAN(); - } - - // Execute function - $retType = Functions::getReturnDateType(); - if ($retType === Functions::RETURNDATE_EXCEL) { - $date = 0; - $calendar = Date::getExcelCalendar(); - if ($calendar != Date::CALENDAR_WINDOWS_1900) { - $date = 1; - } - - return (float) Date::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); - } - if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { - return (int) Date::excelToTimestamp(Date::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 - } - // RETURNDATE_PHP_DATETIME_OBJECT - // Hour has already been normalized (0-23) above - $phpDateObject = new \DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); - - return $phpDateObject; + return DateTimeExcel\Time::funcTime($hour, $minute, $second); } /** @@ -394,6 +181,8 @@ class DateTime * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the date * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. * + * @Deprecated 2.0.0 Use the funcDateValue method in the DateTimeExcel\DateValue class instead + * * Excel Function: * DATEVALUE(dateValue) * @@ -411,186 +200,7 @@ class DateTime */ public static function DATEVALUE($dateValue = 1) { - $dti = new DateTimeImmutable(); - $baseYear = Date::getExcelCalendar(); - $dateValue = trim(Functions::flattenSingleValue($dateValue), '"'); - // Strip any ordinals because they're allowed in Excel (English only) - $dateValue = 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); - - $yearFound = false; - $t1 = explode(' ', $dateValue); - $t = ''; - foreach ($t1 as &$t) { - if ((is_numeric($t)) && ($t > 31)) { - if ($yearFound) { - return Functions::VALUE(); - } - if ($t < 100) { - $t += 1900; - } - $yearFound = true; - } - } - if (count($t1) === 1) { - // We've been fed a time value without any date - return ((strpos($t, ':') === false)) ? Functions::Value() : 0.0; - } - if (count($t1) == 2) { - // We only have two parts of the date: either day/month or month/year - if ($yearFound) { - array_unshift($t1, 1); - } else { - if (is_numeric($t1[1]) && $t1[1] > 29) { - $t1[1] += 1900; - array_unshift($t1, 1); - } else { - $t1[] = $dti->format('Y'); - } - } - } - unset($t); - $dateValue = implode(' ', $t1); - - $PHPDateArray = date_parse($dateValue); - if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { - // If original count was 1, we've already returned. - // If it was 2, we added another. - // Therefore, neither of the first 2 stroks below can fail. - $testVal1 = strtok($dateValue, '- '); - $testVal2 = strtok('- '); - $testVal3 = strtok('- ') ?: $dti->format('Y'); - self::adjustYear($testVal1, $testVal2, $testVal3); - $PHPDateArray = date_parse($testVal1 . '-' . $testVal2 . '-' . $testVal3); - if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { - $PHPDateArray = date_parse($testVal2 . '-' . $testVal1 . '-' . $testVal3); - if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { - return Functions::VALUE(); - } - } - } - - $retValue = Functions::Value(); - if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { - // Execute function - self::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y')); - if ($PHPDateArray['year'] < $baseYear) { - return Functions::VALUE(); - } - self::replaceIfEmpty($PHPDateArray['month'], $dti->format('m')); - self::replaceIfEmpty($PHPDateArray['day'], $dti->format('d')); - $PHPDateArray['hour'] = 0; - $PHPDateArray['minute'] = 0; - $PHPDateArray['second'] = 0; - $month = (int) $PHPDateArray['month']; - $day = (int) $PHPDateArray['day']; - $year = (int) $PHPDateArray['year']; - if (!checkdate($month, $day, $year)) { - return ($year === 1900 && $month === 2 && $day === 29) ? self::returnIn3FormatsFloat(60.0) : Functions::VALUE(); - } - $retValue = is_array($PHPDateArray) ? self::returnIn3FormatsArray($PHPDateArray, true) : Functions::VALUE(); - } - - return $retValue; - } - - /** - * Help reduce perceived complexity of some tests. - * - * @param mixed $value - * @param mixed $altValue - */ - private static function replaceIfEmpty(&$value, $altValue): void - { - $value = $value ?: $altValue; - } - - /** - * Adjust year in ambiguous situations. - */ - private static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void - { - if (!is_numeric($testVal1) || $testVal1 < 31) { - if (!is_numeric($testVal2) || $testVal2 < 12) { - if (is_numeric($testVal3) && $testVal3 < 12) { - $testVal3 += 2000; - } - } - } - } - - /** - * Return result in one of three formats. - * - * @return mixed - */ - private static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false) - { - $retType = Functions::getReturnDateType(); - if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { - return new \DateTime( - $dateArray['year'] - . '-' . $dateArray['month'] - . '-' . $dateArray['day'] - . ' ' . $dateArray['hour'] - . ':' . $dateArray['minute'] - . ':' . $dateArray['second'] - ); - } - $excelDateValue = - Date::formattedPHPToExcel( - $dateArray['year'], - $dateArray['month'], - $dateArray['day'], - $dateArray['hour'], - $dateArray['minute'], - $dateArray['second'] - ); - if ($retType === Functions::RETURNDATE_EXCEL) { - return $noFrac ? floor($excelDateValue) : (float) $excelDateValue; - } - // RETURNDATE_UNIX_TIMESTAMP) - - return (int) Date::excelToTimestamp($excelDateValue); - } - - /** - * Return result in one of three formats. - * - * @return mixed - */ - private static function returnIn3FormatsFloat(float $excelDateValue) - { - $retType = Functions::getReturnDateType(); - if ($retType === Functions::RETURNDATE_EXCEL) { - return $excelDateValue; - } - if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { - return (int) Date::excelToTimestamp($excelDateValue); - } - // RETURNDATE_PHP_DATETIME_OBJECT - - return Date::excelToDateTimeObject($excelDateValue); - } - - /** - * Return result in one of three formats. - * - * @return mixed - */ - private static function returnIn3FormatsObject(\DateTime $PHPDateObject) - { - $retType = Functions::getReturnDateType(); - if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { - return $PHPDateObject; - } - if ($retType === Functions::RETURNDATE_EXCEL) { - return (float) Date::PHPToExcel($PHPDateObject); - } - // RETURNDATE_UNIX_TIMESTAMP - - return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); + return DateTimeExcel\DateValue::funcDateValue($dateValue); } /** @@ -603,6 +213,8 @@ class DateTime * NOTE: When used in a Cell Formula, MS Excel changes the cell format so that it matches the time * format of your regional settings. PhpSpreadsheet does not change cell formatting in this way. * + * @Deprecated 2.0.0 Use the funcTimeValue method in the DateTimeExcel\TimeValue class instead + * * Excel Function: * TIMEVALUE(timeValue) * @@ -616,37 +228,14 @@ class DateTime */ public static function TIMEVALUE($timeValue) { - $timeValue = trim(Functions::flattenSingleValue($timeValue), '"'); - $timeValue = str_replace(['/', '.'], '-', $timeValue); - - $arraySplit = preg_split('/[\/:\-\s]/', $timeValue); - if ((count($arraySplit) == 2 || count($arraySplit) == 3) && $arraySplit[0] > 24) { - $arraySplit[0] = ($arraySplit[0] % 24); - $timeValue = implode(':', $arraySplit); - } - - $PHPDateArray = date_parse($timeValue); - $retValue = Functions::VALUE(); - if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { - // OpenOffice-specific code removed - it works just like Excel - $excelDateValue = Date::formattedPHPToExcel(1900, 1, 1, $PHPDateArray['hour'], $PHPDateArray['minute'], $PHPDateArray['second']) - 1; - - $retType = Functions::getReturnDateType(); - if ($retType === Functions::RETURNDATE_EXCEL) { - $retValue = (float) $excelDateValue; - } elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { - $retValue = (int) $phpDateValue = Date::excelToTimestamp($excelDateValue + 25569) - 3600; - } else { - $retValue = new \DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); - } - } - - return $retValue; + return DateTimeExcel\TimeValue::funcTimeValue($timeValue); } /** * DATEDIF. * + * @Deprecated 2.0.0 Use the funcDateDif method in the DateTimeExcel\DateDif class instead + * * @param mixed $startDate Excel date serial value, PHP date/time stamp, PHP DateTime object * or a standard date string * @param mixed $endDate Excel date serial value, PHP date/time stamp, PHP DateTime object @@ -657,95 +246,7 @@ class DateTime */ public static function DATEDIF($startDate = 0, $endDate = 0, $unit = 'D') { - $startDate = Functions::flattenSingleValue($startDate); - $endDate = Functions::flattenSingleValue($endDate); - $unit = strtoupper(Functions::flattenSingleValue($unit)); - - if (is_string($startDate = self::getDateValue($startDate))) { - return Functions::VALUE(); - } - if (is_string($endDate = self::getDateValue($endDate))) { - return Functions::VALUE(); - } - - // Validate parameters - if ($startDate > $endDate) { - return Functions::NAN(); - } - - // Execute function - $difference = $endDate - $startDate; - - $PHPStartDateObject = Date::excelToDateTimeObject($startDate); - $startDays = $PHPStartDateObject->format('j'); - $startMonths = $PHPStartDateObject->format('n'); - $startYears = $PHPStartDateObject->format('Y'); - - $PHPEndDateObject = Date::excelToDateTimeObject($endDate); - $endDays = $PHPEndDateObject->format('j'); - $endMonths = $PHPEndDateObject->format('n'); - $endYears = $PHPEndDateObject->format('Y'); - - $PHPDiffDateObject = $PHPEndDateObject->diff($PHPStartDateObject); - - switch ($unit) { - case 'D': - $retVal = (int) $difference; - - break; - case 'M': - $retVal = (int) 12 * $PHPDiffDateObject->format('%y') + $PHPDiffDateObject->format('%m'); - - break; - case 'Y': - $retVal = (int) $PHPDiffDateObject->format('%y'); - - break; - case 'MD': - if ($endDays < $startDays) { - $retVal = $endDays; - $PHPEndDateObject->modify('-' . $endDays . ' days'); - $adjustDays = $PHPEndDateObject->format('j'); - $retVal += ($adjustDays - $startDays); - } else { - $retVal = (int) $PHPDiffDateObject->format('%d'); - } - - break; - case 'YM': - $retVal = (int) $PHPDiffDateObject->format('%m'); - - break; - case 'YD': - $retVal = (int) $difference; - if ($endYears > $startYears) { - $isLeapStartYear = $PHPStartDateObject->format('L'); - $wasLeapEndYear = $PHPEndDateObject->format('L'); - - // Adjust end year to be as close as possible as start year - while ($PHPEndDateObject >= $PHPStartDateObject) { - $PHPEndDateObject->modify('-1 year'); - $endYears = $PHPEndDateObject->format('Y'); - } - $PHPEndDateObject->modify('+1 year'); - - // Get the result - $retVal = $PHPEndDateObject->diff($PHPStartDateObject)->days; - - // Adjust for leap years cases - $isLeapEndYear = $PHPEndDateObject->format('L'); - $limit = new \DateTime($PHPEndDateObject->format('Y-02-29')); - if (!$isLeapStartYear && !$wasLeapEndYear && $isLeapEndYear && $PHPEndDateObject >= $limit) { - --$retVal; - } - } - - break; - default: - $retVal = Functions::VALUE(); - } - - return $retVal; + return DateTimeExcel\DateDif::funcDateDif($startDate, $endDate, $unit); } /** @@ -753,43 +254,21 @@ class DateTime * * Returns the number of days between two dates * + * @Deprecated 2.0.0 Use the funcDays method in the DateTimeExcel\Days class instead + * * Excel Function: * DAYS(endDate, startDate) * - * @param DateTimeImmutable|float|int|string $endDate Excel date serial value (float), + * @param DateTimeInterface|float|int|string $endDate Excel date serial value (float), * PHP date timestamp (integer), PHP DateTime object, or a standard date string - * @param DateTimeImmutable|float|int|string $startDate Excel date serial value (float), + * @param 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 */ public static function DAYS($endDate = 0, $startDate = 0) { - $startDate = Functions::flattenSingleValue($startDate); - $endDate = Functions::flattenSingleValue($endDate); - - $startDate = self::getDateValue($startDate); - if (is_string($startDate)) { - return Functions::VALUE(); - } - - $endDate = self::getDateValue($endDate); - if (is_string($endDate)) { - return Functions::VALUE(); - } - - // Execute function - $PHPStartDateObject = Date::excelToDateTimeObject($startDate); - $PHPEndDateObject = Date::excelToDateTimeObject($endDate); - - $diff = $PHPStartDateObject->diff($PHPEndDateObject); - $days = $diff->days; - - if ($diff->invert) { - $days = -$days; - } - - return $days; + return DateTimeExcel\Days::funcDays($endDate, $startDate); } /** @@ -799,6 +278,8 @@ class DateTime * which is used in some accounting calculations. Use this function to help compute payments if * your accounting system is based on twelve 30-day months. * + * @Deprecated 2.0.0 Use the funcDays360 method in the DateTimeExcel\Days360 class instead + * * Excel Function: * DAYS360(startDate,endDate[,method]) * @@ -822,32 +303,7 @@ class DateTime */ public static function DAYS360($startDate = 0, $endDate = 0, $method = false) { - $startDate = Functions::flattenSingleValue($startDate); - $endDate = Functions::flattenSingleValue($endDate); - - if (is_string($startDate = self::getDateValue($startDate))) { - return Functions::VALUE(); - } - if (is_string($endDate = self::getDateValue($endDate))) { - return Functions::VALUE(); - } - - if (!is_bool($method)) { - return Functions::VALUE(); - } - - // Execute function - $PHPStartDateObject = Date::excelToDateTimeObject($startDate); - $startDay = $PHPStartDateObject->format('j'); - $startMonth = $PHPStartDateObject->format('n'); - $startYear = $PHPStartDateObject->format('Y'); - - $PHPEndDateObject = Date::excelToDateTimeObject($endDate); - $endDay = $PHPEndDateObject->format('j'); - $endMonth = $PHPEndDateObject->format('n'); - $endYear = $PHPEndDateObject->format('Y'); - - return self::dateDiff360($startDay, $startMonth, $startYear, $endDay, $endMonth, $endYear, !$method); + return DateTimeExcel\Days360::funcDays360($startDate, $endDate, $method); } /** @@ -858,6 +314,8 @@ class DateTime * Use the YEARFRAC worksheet function to identify the proportion of a whole year's benefits or * obligations to assign to a specific term. * + * @Deprecated 2.0.0 Use the funcYearFrac method in the DateTimeExcel\YearFrac class instead + * * Excel Function: * YEARFRAC(startDate,endDate[,method]) * See https://lists.oasis-open.org/archives/office-formula/200806/msg00039.html @@ -878,78 +336,7 @@ class DateTime */ public static function YEARFRAC($startDate = 0, $endDate = 0, $method = 0) { - $startDate = Functions::flattenSingleValue($startDate); - $endDate = Functions::flattenSingleValue($endDate); - $method = Functions::flattenSingleValue($method); - - if (is_string($startDate = self::getDateValue($startDate))) { - return Functions::VALUE(); - } - if (is_string($endDate = self::getDateValue($endDate))) { - return Functions::VALUE(); - } - if ($startDate > $endDate) { - $temp = $startDate; - $startDate = $endDate; - $endDate = $temp; - } - - if (((is_numeric($method)) && (!is_string($method))) || ($method == '')) { - switch ($method) { - case 0: - return self::DAYS360($startDate, $endDate) / 360; - case 1: - $days = self::DATEDIF($startDate, $endDate); - $startYear = self::YEAR($startDate); - $endYear = self::YEAR($endDate); - $years = $endYear - $startYear + 1; - $startMonth = self::MONTHOFYEAR($startDate); - $startDay = self::DAYOFMONTH($startDate); - $endMonth = self::MONTHOFYEAR($endDate); - $endDay = self::DAYOFMONTH($endDate); - $startMonthDay = 100 * $startMonth + $startDay; - $endMonthDay = 100 * $endMonth + $endDay; - if ($years == 1) { - if (self::isLeapYear($endYear)) { - $tmpCalcAnnualBasis = 366; - } else { - $tmpCalcAnnualBasis = 365; - } - } elseif ($years == 2 && $startMonthDay >= $endMonthDay) { - if (self::isLeapYear($startYear)) { - if ($startMonthDay <= 229) { - $tmpCalcAnnualBasis = 366; - } else { - $tmpCalcAnnualBasis = 365; - } - } elseif (self::isLeapYear($endYear)) { - if ($endMonthDay >= 229) { - $tmpCalcAnnualBasis = 366; - } else { - $tmpCalcAnnualBasis = 365; - } - } else { - $tmpCalcAnnualBasis = 365; - } - } else { - $tmpCalcAnnualBasis = 0; - for ($year = $startYear; $year <= $endYear; ++$year) { - $tmpCalcAnnualBasis += self::isLeapYear($year) ? 366 : 365; - } - $tmpCalcAnnualBasis /= $years; - } - - return $days / $tmpCalcAnnualBasis; - case 2: - return self::DATEDIF($startDate, $endDate) / 360; - case 3: - return self::DATEDIF($startDate, $endDate) / 365; - case 4: - return self::DAYS360($startDate, $endDate, true) / 360; - } - } - - return Functions::VALUE(); + return DateTimeExcel\YearFrac::funcYearFrac($startDate, $endDate, $method); } /** @@ -960,6 +347,8 @@ class DateTime * Use NETWORKDAYS to calculate employee benefits that accrue based on the number of days * worked during a specific term. * + * @Deprecated 2.0.0 Use the funcNetworkDays method in the DateTimeExcel\NetworkDays class instead + * * Excel Function: * NETWORKDAYS(startDate,endDate[,holidays[,holiday[,...]]]) * @@ -972,62 +361,7 @@ class DateTime */ public static function NETWORKDAYS($startDate, $endDate, ...$dateArgs) { - // Retrieve the mandatory start and end date that are referenced in the function definition - $startDate = Functions::flattenSingleValue($startDate); - $endDate = Functions::flattenSingleValue($endDate); - // Get the optional days - $dateArgs = Functions::flattenArray($dateArgs); - - // Validate the start and end dates - if (is_string($startDate = $sDate = self::getDateValue($startDate))) { - return Functions::VALUE(); - } - $startDate = (float) floor($startDate); - if (is_string($endDate = $eDate = self::getDateValue($endDate))) { - return Functions::VALUE(); - } - $endDate = (float) floor($endDate); - - if ($sDate > $eDate) { - $startDate = $eDate; - $endDate = $sDate; - } - - // Execute function - $startDoW = 6 - self::WEEKDAY($startDate, 2); - if ($startDoW < 0) { - $startDoW = 5; - } - $endDoW = self::WEEKDAY($endDate, 2); - if ($endDoW >= 6) { - $endDoW = 0; - } - - $wholeWeekDays = floor(($endDate - $startDate) / 7) * 5; - $partWeekDays = $endDoW + $startDoW; - if ($partWeekDays > 5) { - $partWeekDays -= 5; - } - - // Test any extra holiday parameters - $holidayCountedArray = []; - foreach ($dateArgs as $holidayDate) { - if (is_string($holidayDate = self::getDateValue($holidayDate))) { - return Functions::VALUE(); - } - if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { - if ((self::WEEKDAY($holidayDate, 2) < 6) && (!in_array($holidayDate, $holidayCountedArray))) { - --$partWeekDays; - $holidayCountedArray[] = $holidayDate; - } - } - } - - if ($sDate > $eDate) { - return 0 - ($wholeWeekDays + $partWeekDays); - } - - return $wholeWeekDays + $partWeekDays; + return DateTimeExcel\NetworkDays::funcNetworkDays($startDate, $endDate, ...$dateArgs); } /** @@ -1038,6 +372,8 @@ class DateTime * Use WORKDAY to exclude weekends or holidays when you calculate invoice due dates, expected * delivery times, or the number of days of work performed. * + * @Deprecated 2.0.0 Use the funcWorkDay method in the DateTimeExcel\WorkDay class instead + * * Excel Function: * WORKDAY(startDate,endDays[,holidays[,holiday[,...]]]) * @@ -1052,84 +388,7 @@ class DateTime */ public static function WORKDAY($startDate, $endDays, ...$dateArgs) { - // Retrieve the mandatory start date and days that are referenced in the function definition - $startDate = Functions::flattenSingleValue($startDate); - $endDays = Functions::flattenSingleValue($endDays); - // Get the optional days - $dateArgs = Functions::flattenArray($dateArgs); - - if ((is_string($startDate = self::getDateValue($startDate))) || (!is_numeric($endDays))) { - return Functions::VALUE(); - } - $startDate = (float) floor($startDate); - $endDays = (int) floor($endDays); - // If endDays is 0, we always return startDate - if ($endDays == 0) { - return $startDate; - } - - $decrementing = $endDays < 0; - - // Adjust the start date if it falls over a weekend - - $startDoW = self::WEEKDAY($startDate, 3); - if (self::WEEKDAY($startDate, 3) >= 5) { - $startDate += ($decrementing) ? -$startDoW + 4 : 7 - $startDoW; - ($decrementing) ? $endDays++ : $endDays--; - } - - // Add endDays - $endDate = (float) $startDate + ((int) ($endDays / 5) * 7) + ($endDays % 5); - - // Adjust the calculated end date if it falls over a weekend - $endDoW = self::WEEKDAY($endDate, 3); - if ($endDoW >= 5) { - $endDate += ($decrementing) ? -$endDoW + 4 : 7 - $endDoW; - } - - // Test any extra holiday parameters - if (!empty($dateArgs)) { - $holidayCountedArray = $holidayDates = []; - foreach ($dateArgs as $holidayDate) { - if (($holidayDate !== null) && (trim($holidayDate) > '')) { - if (is_string($holidayDate = self::getDateValue($holidayDate))) { - return Functions::VALUE(); - } - if (self::WEEKDAY($holidayDate, 3) < 5) { - $holidayDates[] = $holidayDate; - } - } - } - if ($decrementing) { - rsort($holidayDates, SORT_NUMERIC); - } else { - sort($holidayDates, SORT_NUMERIC); - } - foreach ($holidayDates as $holidayDate) { - if ($decrementing) { - if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) { - if (!in_array($holidayDate, $holidayCountedArray)) { - --$endDate; - $holidayCountedArray[] = $holidayDate; - } - } - } else { - if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { - if (!in_array($holidayDate, $holidayCountedArray)) { - ++$endDate; - $holidayCountedArray[] = $holidayDate; - } - } - } - // Adjust the calculated end date if it falls over a weekend - $endDoW = self::WEEKDAY($endDate, 3); - if ($endDoW >= 5) { - $endDate += ($decrementing) ? -$endDoW + 4 : 7 - $endDoW; - } - } - } - - return self::returnIn3FormatsFloat($endDate); + return DateTimeExcel\WorkDay::funcWorkDay($startDate, $endDays, ...$dateArgs); } /** @@ -1138,6 +397,8 @@ class DateTime * Returns the day of the month, for a specified date. The day is given as an integer * ranging from 1 to 31. * + * @Deprecated 2.0.0 Use the funcDay method in the DateTimeExcel\Day class instead + * * Excel Function: * DAY(dateValue) * @@ -1148,27 +409,7 @@ class DateTime */ public static function DAYOFMONTH($dateValue = 1) { - $dateValue = Functions::flattenSingleValue($dateValue); - - if ($dateValue === null || is_bool($dateValue)) { - return (int) $dateValue; - } - if (is_string($dateValue = self::getDateValue($dateValue))) { - return Functions::VALUE(); - } - - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { - if ($dateValue < 0.0) { - return Functions::NAN(); - } elseif ($dateValue < 1.0) { - return 0; - } - } - - // Execute function - $PHPDateObject = Date::excelToDateTimeObject($dateValue); - - return (int) $PHPDateObject->format('j'); + return DateTimeExcel\Day::funcDay($dateValue); } /** @@ -1177,6 +418,8 @@ class DateTime * Returns the day of the week for a specified date. The day is given as an integer * ranging from 0 to 7 (dependent on the requested style). * + * @Deprecated 2.0.0 Use the funcWeekDay method in the DateTimeExcel\WeekDay class instead + * * Excel Function: * WEEKDAY(dateValue[,style]) * @@ -1191,70 +434,169 @@ class DateTime */ public static function WEEKDAY($dateValue = 1, $style = 1) { - $dateValue = Functions::flattenSingleValue($dateValue); - self::nullFalseTrueToNumber($dateValue); - $style = Functions::flattenSingleValue($style); - - if (!is_numeric($style)) { - return Functions::VALUE(); - } elseif (($style < 1) || ($style > 3)) { - return Functions::NAN(); - } - $style = floor($style); - - $dateValue = self::getDateValue($dateValue); - if (is_string($dateValue)) { - return Functions::VALUE(); - } - if ($dateValue < 0.0) { - return Functions::NAN(); - } - - // Execute function - $PHPDateObject = Date::excelToDateTimeObject($dateValue); - self::silly1900($PHPDateObject); - $DoW = (int) $PHPDateObject->format('w'); - - switch ($style) { - case 1: - ++$DoW; - - break; - case 2: - if ($DoW === 0) { - $DoW = 7; - } - - break; - case 3: - if ($DoW === 0) { - $DoW = 7; - } - --$DoW; - - break; - } - - return $DoW; + return DateTimeExcel\WeekDay::funcWeekDay($dateValue, $style); } + /** + * STARTWEEK_SUNDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_SUNDAY instead + */ const STARTWEEK_SUNDAY = 1; + + /** + * STARTWEEK_MONDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_MONDAY instead + */ const STARTWEEK_MONDAY = 2; + + /** + * STARTWEEK_MONDAY_ALT. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_MONDAY_ALT instead + */ const STARTWEEK_MONDAY_ALT = 11; + + /** + * STARTWEEK_TUESDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_TUESDAY instead + */ const STARTWEEK_TUESDAY = 12; + + /** + * STARTWEEK_WEDNESDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_WEDNESDAY instead + */ const STARTWEEK_WEDNESDAY = 13; + + /** + * STARTWEEK_THURSDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_THURSDAY instead + */ const STARTWEEK_THURSDAY = 14; + + /** + * STARTWEEK_FRIDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_FRIDAY instead + */ const STARTWEEK_FRIDAY = 15; + + /** + * STARTWEEK_SATURDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_SATURDAY instead + */ const STARTWEEK_SATURDAY = 16; + + /** + * STARTWEEK_SUNDAY_ALT. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_SUNDAY_ALT instead + */ const STARTWEEK_SUNDAY_ALT = 17; + + /** + * DOW_SUNDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\DOW_SUNDAY instead + */ const DOW_SUNDAY = 1; + + /** + * DOW_MONDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\DOW_MONDAY instead + */ const DOW_MONDAY = 2; + + /** + * DOW_TUESDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\DOW_TUESDAY instead + */ const DOW_TUESDAY = 3; + + /** + * DOW_WEDNESDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\DOW_WEDNESDAY instead + */ const DOW_WEDNESDAY = 4; + + /** + * DOW_THURSDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\DOW_THURSDAY instead + */ const DOW_THURSDAY = 5; + + /** + * DOW_FRIDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\DOW_FRIDAY instead + */ const DOW_FRIDAY = 6; + + /** + * DOW_SATURDAY. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\DOW_SATURDAY instead + */ const DOW_SATURDAY = 7; + + /** + * STARTWEEK_MONDAY_ISO. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\STARTWEEK_MONDAY_ISO instead + */ const STARTWEEK_MONDAY_ISO = 21; + + /** + * METHODARR. + * + * @Deprecated 2.0.0 + * + * @see Use DateTimeExcel\Constants\METHODARR instead + */ const METHODARR = [ self::STARTWEEK_SUNDAY => self::DOW_SUNDAY, self::DOW_MONDAY, @@ -1278,6 +620,8 @@ class DateTime * three days or less in the first week of January, the WEEKNUM function returns week numbers * that are incorrect according to the European standard. * + * @Deprecated 2.0.0 Use the funcWeekNum method in the DateTimeExcel\WeekNum class instead + * * Excel Function: * WEEKNUM(dateValue[,style]) * @@ -1299,67 +643,7 @@ class DateTime */ public static function WEEKNUM($dateValue = 1, $method = self::STARTWEEK_SUNDAY) { - $origDateValueNull = $dateValue === null; - $dateValue = Functions::flattenSingleValue($dateValue); - $method = Functions::flattenSingleValue($method); - if (!is_numeric($method)) { - return Functions::VALUE(); - } - - $method = (int) $method; - if (!array_key_exists($method, self::METHODARR)) { - return Functions::NaN(); - } - $method = self::METHODARR[$method]; - if ($dateValue === null) { // boolean not allowed - // This seems to be an additional Excel bug. - if (self::buggyWeekNum1900($method)) { - return 0; - } - //$dateValue = 1; - $dateValue = (Date::getExcelCalendar() === DATE::CALENDAR_MAC_1904) ? 0 : 1; - } - - $dateValue = self::getDateValue($dateValue); - if (is_string($dateValue)) { - return Functions::VALUE(); - } - if ($dateValue < 0.0) { - return Functions::NAN(); - } - - // Execute function - $PHPDateObject = Date::excelToDateTimeObject($dateValue); - if ($method == self::STARTWEEK_MONDAY_ISO) { - self::silly1900($PHPDateObject); - - return (int) $PHPDateObject->format('W'); - } - if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) { - return 0; - } - self::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches - $dayOfYear = $PHPDateObject->format('z'); - $PHPDateObject->modify('-' . $dayOfYear . ' days'); - $firstDayOfFirstWeek = $PHPDateObject->format('w'); - $daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; - $daysInFirstWeek += 7 * !$daysInFirstWeek; - $endFirstWeek = $daysInFirstWeek - 1; - $weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7); - - return (int) $weekOfYear; - } - - private static function buggyWeekNum1900(int $method): bool - { - return $method === self::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_WINDOWS_1900; - } - - private static function buggyWeekNum1904(int $method, bool $origNull, \DateTime $dateObject): bool - { - // This appears to be another Excel bug. - - return $method === self::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_MAC_1904 && !$origNull && $dateObject->format('Y-m-d') === '1904-01-01'; + return DateTimeExcel\WeekNum::funcWeekNum($dateValue, $method); } /** @@ -1367,6 +651,8 @@ class DateTime * * Returns the ISO 8601 week number of the year for a specified date. * + * @Deprecated 2.0.0 Use the funcIsoWeeknum method in the DateTimeExcel\Isoweeknum class instead + * * Excel Function: * ISOWEEKNUM(dateValue) * @@ -1377,22 +663,7 @@ class DateTime */ public static function ISOWEEKNUM($dateValue = 1) { - $dateValue = Functions::flattenSingleValue($dateValue); - self::nullFalseTrueToNumber($dateValue); - - $dateValue = self::getDateValue($dateValue); - if (!is_numeric($dateValue)) { - return Functions::VALUE(); - } - if ($dateValue < 0.0) { - return Functions::NAN(); - } - - // Execute function - $PHPDateObject = Date::excelToDateTimeObject($dateValue); - self::silly1900($PHPDateObject); - - return (int) $PHPDateObject->format('W'); + return DateTimeExcel\IsoweekNum::funcIsoWeekNum($dateValue); } /** @@ -1401,6 +672,8 @@ class DateTime * Returns the month of a date represented by a serial number. * The month is given as an integer, ranging from 1 (January) to 12 (December). * + * @Deprecated 2.0.0 Use the funcMonth method in the DateTimeExcel\Month class instead + * * Excel Function: * MONTH(dateValue) * @@ -1411,21 +684,7 @@ class DateTime */ public static function MONTHOFYEAR($dateValue = 1) { - $dateValue = Functions::flattenSingleValue($dateValue); - - if (empty($dateValue)) { - $dateValue = 1; - } - if (is_string($dateValue = self::getDateValue($dateValue))) { - return Functions::VALUE(); - } elseif ($dateValue < 0.0) { - return Functions::NAN(); - } - - // Execute function - $PHPDateObject = Date::excelToDateTimeObject($dateValue); - - return (int) $PHPDateObject->format('n'); + return DateTimeExcel\Month::funcMonth($dateValue); } /** @@ -1434,6 +693,8 @@ class DateTime * Returns the year corresponding to a date. * The year is returned as an integer in the range 1900-9999. * + * @Deprecated 2.0.0 Use the funcYear method in the DateTimeExcel\Year class instead + * * Excel Function: * YEAR(dateValue) * @@ -1444,20 +705,7 @@ class DateTime */ public static function YEAR($dateValue = 1) { - $dateValue = Functions::flattenSingleValue($dateValue); - - if ($dateValue === null) { - $dateValue = 1; - } elseif (is_string($dateValue = self::getDateValue($dateValue))) { - return Functions::VALUE(); - } elseif ($dateValue < 0.0) { - return Functions::NAN(); - } - - // Execute function - $PHPDateObject = Date::excelToDateTimeObject($dateValue); - - return (int) $PHPDateObject->format('Y'); + return DateTimeExcel\Year::funcYear($dateValue); } /** @@ -1466,6 +714,8 @@ class DateTime * Returns the hour of a time value. * The hour is given as an integer, ranging from 0 (12:00 A.M.) to 23 (11:00 P.M.). * + * @Deprecated 2.0.0 Use the funcHour method in the DateTimeExcel\Hour class instead + * * Excel Function: * HOUR(timeValue) * @@ -1476,24 +726,7 @@ class DateTime */ public static function HOUROFDAY($timeValue = 0) { - $timeValue = Functions::flattenSingleValue($timeValue); - - if (!is_numeric($timeValue)) { - // Gnumeric test removed - it operates like Excel - $timeValue = self::getTimeValue($timeValue); - if (is_string($timeValue)) { - return Functions::VALUE(); - } - } - // Execute function - if ($timeValue >= 1) { - $timeValue = fmod($timeValue, 1); - } elseif ($timeValue < 0.0) { - return Functions::NAN(); - } - $timeValue = Date::excelToTimestamp($timeValue); - - return (int) gmdate('G', $timeValue); + return DateTimeExcel\Hour::funcHour($timeValue); } /** @@ -1502,6 +735,8 @@ class DateTime * Returns the minutes of a time value. * The minute is given as an integer, ranging from 0 to 59. * + * @Deprecated 2.0.0 Use the funcMinute method in the DateTimeExcel\Minute class instead + * * Excel Function: * MINUTE(timeValue) * @@ -1512,24 +747,7 @@ class DateTime */ public static function MINUTE($timeValue = 0) { - $timeValue = $timeTester = Functions::flattenSingleValue($timeValue); - - if (!is_numeric($timeValue)) { - // Gnumeric test removed - it operates like Excel - $timeValue = self::getTimeValue($timeValue); - if (is_string($timeValue)) { - return Functions::VALUE(); - } - } - // Execute function - if ($timeValue >= 1) { - $timeValue = fmod($timeValue, 1); - } elseif ($timeValue < 0.0) { - return Functions::NAN(); - } - $timeValue = Date::excelToTimestamp($timeValue); - - return (int) gmdate('i', $timeValue); + return DateTimeExcel\Minute::funcMinute($timeValue); } /** @@ -1538,6 +756,8 @@ class DateTime * Returns the seconds of a time value. * The second is given as an integer in the range 0 (zero) to 59. * + * @Deprecated 2.0.0 Use the funcSecond method in the DateTimeExcel\Second class instead + * * Excel Function: * SECOND(timeValue) * @@ -1548,24 +768,7 @@ class DateTime */ public static function SECOND($timeValue = 0) { - $timeValue = Functions::flattenSingleValue($timeValue); - - if (!is_numeric($timeValue)) { - // Gnumeric test removed - it operates like Excel - $timeValue = self::getTimeValue($timeValue); - if (is_string($timeValue)) { - return Functions::VALUE(); - } - } - // Execute function - if ($timeValue >= 1) { - $timeValue = fmod($timeValue, 1); - } elseif ($timeValue < 0.0) { - return Functions::NAN(); - } - $timeValue = Date::excelToTimestamp($timeValue); - - return (int) gmdate('s', $timeValue); + return DateTimeExcel\Second::funcSecond($timeValue); } /** @@ -1576,6 +779,8 @@ class DateTime * Use EDATE to calculate maturity dates or due dates that fall on the same day of the month * as the date of issue. * + * @Deprecated 2.0.0 Use the funcEDate method in the DateTimeExcel\EDate class instead + * * Excel Function: * EDATE(dateValue,adjustmentMonths) * @@ -1590,22 +795,7 @@ class DateTime */ public static function EDATE($dateValue = 1, $adjustmentMonths = 0) { - $dateValue = Functions::flattenSingleValue($dateValue); - $adjustmentMonths = Functions::flattenSingleValue($adjustmentMonths); - - if (!is_numeric($adjustmentMonths)) { - return Functions::VALUE(); - } - $adjustmentMonths = floor($adjustmentMonths); - - if (is_string($dateValue = self::getDateValue($dateValue))) { - return Functions::VALUE(); - } - - // Execute function - $PHPDateObject = self::adjustDateByMonths($dateValue, $adjustmentMonths); - - return self::returnIn3FormatsObject($PHPDateObject); + return DateTimeExcel\EDate::funcEDate($dateValue, $adjustmentMonths); } /** @@ -1615,6 +805,8 @@ class DateTime * before or after start_date. * Use EOMONTH to calculate maturity dates or due dates that fall on the last day of the month. * + * @Deprecated 2.0.0 Use the funcEoMonth method in the DateTimeExcel\EoMonth class instead + * * Excel Function: * EOMONTH(dateValue,adjustmentMonths) * @@ -1629,49 +821,6 @@ class DateTime */ public static function EOMONTH($dateValue = 1, $adjustmentMonths = 0) { - $dateValue = Functions::flattenSingleValue($dateValue); - $adjustmentMonths = Functions::flattenSingleValue($adjustmentMonths); - - if (!is_numeric($adjustmentMonths)) { - return Functions::VALUE(); - } - $adjustmentMonths = floor($adjustmentMonths); - - if (is_string($dateValue = self::getDateValue($dateValue))) { - return Functions::VALUE(); - } - - // Execute function - $PHPDateObject = self::adjustDateByMonths($dateValue, $adjustmentMonths + 1); - $adjustDays = (int) $PHPDateObject->format('d'); - $adjustDaysString = '-' . $adjustDays . ' days'; - $PHPDateObject->modify($adjustDaysString); - - return self::returnIn3FormatsObject($PHPDateObject); - } - - /** - * Many functions accept null/false/true argument treated as 0/0/1. - * - * @param mixed $number - */ - private static function nullFalseTrueToNumber(&$number): void - { - $number = Functions::flattenSingleValue($number); - $baseYear = Date::getExcelCalendar(); - $nullVal = $baseYear === DATE::CALENDAR_MAC_1904 ? 0 : 1; - if ($number === null) { - $number = $nullVal; - } elseif (is_bool($number)) { - $number = $nullVal + (int) $number; - } - } - - private static function silly1900(\DateTime $PHPDateObject, string $mod = '-1 day'): void - { - $isoDate = $PHPDateObject->format('c'); - if ($isoDate < '1900-03-01') { - $PHPDateObject->modify($mod); - } + return DateTimeExcel\EoMonth::funcEoMonth($dateValue, $adjustmentMonths); } } diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Constants.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Constants.php new file mode 100644 index 00000000..da1b81c1 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Constants.php @@ -0,0 +1,37 @@ + self::DOW_SUNDAY, + self::DOW_MONDAY, + self::STARTWEEK_MONDAY_ALT => self::DOW_MONDAY, + self::DOW_TUESDAY, + self::DOW_WEDNESDAY, + self::DOW_THURSDAY, + self::DOW_FRIDAY, + self::DOW_SATURDAY, + self::DOW_SUNDAY, + self::STARTWEEK_MONDAY_ISO => self::STARTWEEK_MONDAY_ISO, + ]; +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php new file mode 100644 index 00000000..ace22cbf --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php @@ -0,0 +1,146 @@ +getMessage(); + } + + // Execute function + $PHPStartDateObject = Date::excelToDateTimeObject($startDate); + $startDays = (int) $PHPStartDateObject->format('j'); + $startMonths = (int) $PHPStartDateObject->format('n'); + $startYears = (int) $PHPStartDateObject->format('Y'); + + $PHPEndDateObject = Date::excelToDateTimeObject($endDate); + $endDays = (int) $PHPEndDateObject->format('j'); + $endMonths = (int) $PHPEndDateObject->format('n'); + $endYears = (int) $PHPEndDateObject->format('Y'); + + $PHPDiffDateObject = $PHPEndDateObject->diff($PHPStartDateObject); + + $retVal = false; + $retVal = self::replaceRetValue($retVal, $unit, 'D') ?? self::datedifD($difference); + $retVal = self::replaceRetValue($retVal, $unit, 'M') ?? self::datedifM($PHPDiffDateObject); + $retVal = self::replaceRetValue($retVal, $unit, 'MD') ?? self::datedifMD($startDays, $endDays, $PHPEndDateObject, $PHPDiffDateObject); + $retVal = self::replaceRetValue($retVal, $unit, 'Y') ?? self::datedifY($PHPDiffDateObject); + $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; + } + + private static function initialDiff(float $startDate, float $endDate): float + { + // Validate parameters + if ($startDate > $endDate) { + throw new Exception(Functions::NAN()); + } + + return $endDate - $startDate; + } + + /** + * Decide whether it's time to set retVal. + * + * @param bool|int $retVal + * + * @return null|bool|int + */ + private static function replaceRetValue($retVal, string $unit, string $compare) + { + if ($retVal !== false || $unit !== $compare) { + return $retVal; + } + + return null; + } + + private static function datedifD(float $difference): int + { + return (int) $difference; + } + + private static function datedifM(DateInterval $PHPDiffDateObject): int + { + return (int) 12 * $PHPDiffDateObject->format('%y') + $PHPDiffDateObject->format('%m'); + } + + private static function datedifMD(int $startDays, int $endDays, DateTime $PHPEndDateObject, DateInterval $PHPDiffDateObject): int + { + if ($endDays < $startDays) { + $retVal = $endDays; + $PHPEndDateObject->modify('-' . $endDays . ' days'); + $adjustDays = (int) $PHPEndDateObject->format('j'); + $retVal += ($adjustDays - $startDays); + } else { + $retVal = (int) $PHPDiffDateObject->format('%d'); + } + + return $retVal; + } + + private static function datedifY(DateInterval $PHPDiffDateObject): int + { + return (int) $PHPDiffDateObject->format('%y'); + } + + private static function datedifYD(float $difference, int $startYears, int $endYears, DateTime $PHPStartDateObject, DateTime $PHPEndDateObject): int + { + $retVal = (int) $difference; + if ($endYears > $startYears) { + $isLeapStartYear = $PHPStartDateObject->format('L'); + $wasLeapEndYear = $PHPEndDateObject->format('L'); + + // Adjust end year to be as close as possible as start year + while ($PHPEndDateObject >= $PHPStartDateObject) { + $PHPEndDateObject->modify('-1 year'); + $endYears = $PHPEndDateObject->format('Y'); + } + $PHPEndDateObject->modify('+1 year'); + + // Get the result + $retVal = $PHPEndDateObject->diff($PHPStartDateObject)->days; + + // Adjust for leap years cases + $isLeapEndYear = $PHPEndDateObject->format('L'); + $limit = new DateTime($PHPEndDateObject->format('Y-02-29')); + if (!$isLeapStartYear && !$wasLeapEndYear && $isLeapEndYear && $PHPEndDateObject >= $limit) { + --$retVal; + } + } + + return (int) $retVal; + } + + private static function datedifYM(DateInterval $PHPDiffDateObject): int + { + return (int) $PHPDiffDateObject->format('%m'); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php new file mode 100644 index 00000000..3c15d06a --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -0,0 +1,151 @@ + 31)) { + if ($yearFound) { + return Functions::VALUE(); + } + if ($t < 100) { + $t += 1900; + } + $yearFound = true; + } + } + if (count($t1) === 1) { + // We've been fed a time value without any date + return ((strpos($t, ':') === false)) ? Functions::Value() : 0.0; + } + unset($t); + + $dateValue = self::t1ToString($t1, $dti, $yearFound); + + $PHPDateArray = self::setUpArray($dateValue, $dti); + + return self::finalResults($PHPDateArray, $dti, $baseYear); + } + + private static function t1ToString(array $t1, DateTimeImmutable $dti, bool $yearFound): string + { + if (count($t1) == 2) { + // We only have two parts of the date: either day/month or month/year + if ($yearFound) { + array_unshift($t1, 1); + } else { + if (is_numeric($t1[1]) && $t1[1] > 29) { + $t1[1] += 1900; + array_unshift($t1, 1); + } else { + $t1[] = $dti->format('Y'); + } + } + } + $dateValue = implode(' ', $t1); + + return $dateValue; + } + + /** + * Parse date. + * + * @return array|bool + */ + private static function setUpArray(string $dateValue, DateTimeImmutable $dti) + { + $PHPDateArray = date_parse($dateValue); + if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { + // If original count was 1, we've already returned. + // If it was 2, we added another. + // Therefore, neither of the first 2 stroks below can fail. + $testVal1 = strtok($dateValue, '- '); + $testVal2 = strtok('- '); + $testVal3 = strtok('- ') ?: $dti->format('Y'); + Helpers::adjustYear($testVal1, $testVal2, $testVal3); + $PHPDateArray = date_parse($testVal1 . '-' . $testVal2 . '-' . $testVal3); + if (($PHPDateArray === false) || ($PHPDateArray['error_count'] > 0)) { + $PHPDateArray = date_parse($testVal2 . '-' . $testVal1 . '-' . $testVal3); + } + } + + return $PHPDateArray; + } + + /** + * Final results. + * + * @param array|false $PHPDateArray + * + * @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 + */ + private static function finalResults($PHPDateArray, DateTimeImmutable $dti, int $baseYear) + { + $retValue = Functions::Value(); + if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { + // Execute function + Helpers::replaceIfEmpty($PHPDateArray['year'], $dti->format('Y')); + if ($PHPDateArray['year'] < $baseYear) { + return Functions::VALUE(); + } + Helpers::replaceIfEmpty($PHPDateArray['month'], $dti->format('m')); + Helpers::replaceIfEmpty($PHPDateArray['day'], $dti->format('d')); + $PHPDateArray['hour'] = 0; + $PHPDateArray['minute'] = 0; + $PHPDateArray['second'] = 0; + $month = (int) $PHPDateArray['month']; + $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(); + } + $retValue = Helpers::returnIn3FormatsArray($PHPDateArray, true); + } + + return $retValue; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Datefunc.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Datefunc.php new file mode 100644 index 00000000..ec8be2df --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Datefunc.php @@ -0,0 +1,168 @@ +getMessage(); + } + + // Execute function + $excelDateValue = Date::formattedPHPToExcel($year, $month, $day); + + return Helpers::returnIn3FormatsFloat($excelDateValue); + } + + /** + * Convert year from multiple formats to int. + * + * @param mixed $year + */ + private static function getYear($year, int $baseYear): int + { + $year = Functions::flattenSingleValue($year); + $year = ($year !== null) ? StringHelper::testStringAsNumeric($year) : 0; + if (!is_numeric($year)) { + throw new Exception(Functions::VALUE()); + } + $year = (int) $year; + + if ($year < ($baseYear - 1900)) { + throw new Exception(Functions::NAN()); + } + if ((($baseYear - 1900) !== 0) && ($year < $baseYear) && ($year >= 1900)) { + throw new Exception(Functions::NAN()); + } + + if (($year < $baseYear) && ($year >= ($baseYear - 1900))) { + $year += 1900; + } + + return $year; + } + + /** + * Convert month from multiple formats to int. + * + * @param mixed $month + */ + private static function getMonth($month): int + { + $month = Functions::flattenSingleValue($month); + + if (($month !== null) && (!is_numeric($month))) { + $month = Date::monthStringToNumber($month); + } + + $month = ($month !== null) ? StringHelper::testStringAsNumeric($month) : 0; + if (!is_numeric($month)) { + throw new Exception(Functions::VALUE()); + } + + return (int) $month; + } + + /** + * Convert day from multiple formats to int. + * + * @param mixed $day + */ + private static function getDay($day): int + { + $day = Functions::flattenSingleValue($day); + + if (($day !== null) && (!is_numeric($day))) { + $day = Date::dayStringToNumber($day); + } + + $day = ($day !== null) ? StringHelper::testStringAsNumeric($day) : 0; + if (!is_numeric($day)) { + throw new Exception(Functions::VALUE()); + } + + return (int) $day; + } + + private static function adjustYearMonth(int &$year, int &$month, int $baseYear): void + { + if ($month < 1) { + // Handle year/month adjustment if month < 1 + --$month; + $year += ceil($month / 12) - 1; + $month = 13 - abs($month % 12); + } elseif ($month > 12) { + // Handle year/month adjustment if month > 12 + $year += floor($month / 12); + $month = ($month % 12); + } + + // Re-validate the year parameter after adjustments + if (($year < $baseYear) || ($year >= 10000)) { + throw new Exception(Functions::NAN()); + } + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Day.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Day.php new file mode 100644 index 00000000..6ab27184 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Day.php @@ -0,0 +1,61 @@ += 0) { + return $weirdResult; + } + + try { + $dateValue = Helpers::getDateValue($dateValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Execute function + $PHPDateObject = Date::excelToDateTimeObject($dateValue); + + return (int) $PHPDateObject->format('j'); + } + + private static function weirdCondition($dateValue): int + { + // Excel does not treat 0 consistently for DAY vs. (MONTH or YEAR) + if (Date::getExcelCalendar() === DATE::CALENDAR_WINDOWS_1900 && Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { + if (is_bool($dateValue)) { + return (int) $dateValue; + } + if ($dateValue === null) { + return 0; + } + if (is_numeric($dateValue) && $dateValue < 1 && $dateValue >= 0) { + return 0; + } + } + + return -1; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php new file mode 100644 index 00000000..2c814e8e --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days.php @@ -0,0 +1,51 @@ +getMessage(); + } + + // Execute function + $PHPStartDateObject = Date::excelToDateTimeObject($startDate); + $PHPEndDateObject = Date::excelToDateTimeObject($endDate); + + $days = Functions::VALUE(); + $diff = $PHPStartDateObject->diff($PHPEndDateObject); + if ($diff !== false && !is_bool($diff->days)) { + $days = $diff->days; + if ($diff->invert) { + $days = -$days; + } + } + + return $days; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php new file mode 100644 index 00000000..068ea2bc --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php @@ -0,0 +1,106 @@ +getMessage(); + } + + if (!is_bool($method)) { + return Functions::VALUE(); + } + + // Execute function + $PHPStartDateObject = Date::excelToDateTimeObject($startDate); + $startDay = $PHPStartDateObject->format('j'); + $startMonth = $PHPStartDateObject->format('n'); + $startYear = $PHPStartDateObject->format('Y'); + + $PHPEndDateObject = Date::excelToDateTimeObject($endDate); + $endDay = $PHPEndDateObject->format('j'); + $endMonth = $PHPEndDateObject->format('n'); + $endYear = $PHPEndDateObject->format('Y'); + + return self::dateDiff360((int) $startDay, (int) $startMonth, (int) $startYear, (int) $endDay, (int) $endMonth, (int) $endYear, !$method); + } + + /** + * Return the number of days between two dates based on a 360 day calendar. + */ + private static function dateDiff360(int $startDay, int $startMonth, int $startYear, int $endDay, int $endMonth, int $endYear, bool $methodUS): int + { + $startDay = self::getStartDay($startDay, $startMonth, $startYear, $methodUS); + $endDay = self::getEndDay($endDay, $endMonth, $endYear, $startDay, $methodUS); + + return $endDay + $endMonth * 30 + $endYear * 360 - $startDay - $startMonth * 30 - $startYear * 360; + } + + private static function getStartDay(int $startDay, int $startMonth, int $startYear, bool $methodUS): int + { + if ($startDay == 31) { + --$startDay; + } elseif ($methodUS && ($startMonth == 2 && ($startDay == 29 || ($startDay == 28 && !Helpers::isLeapYear($startYear))))) { + $startDay = 30; + } + + return $startDay; + } + + private static function getEndDay(int $endDay, int &$endMonth, int &$endYear, int $startDay, bool $methodUS): int + { + if ($endDay == 31) { + if ($methodUS && $startDay != 30) { + $endDay = 1; + if ($endMonth == 12) { + ++$endYear; + $endMonth = 1; + } else { + ++$endMonth; + } + } else { + $endDay = 30; + } + } + + return $endDay; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/EDate.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/EDate.php new file mode 100644 index 00000000..43af694f --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/EDate.php @@ -0,0 +1,45 @@ +getMessage(); + } + $adjustmentMonths = floor($adjustmentMonths); + + // Execute function + $PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths); + + return Helpers::returnIn3FormatsObject($PHPDateObject); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/EoMonth.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/EoMonth.php new file mode 100644 index 00000000..6b39a609 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/EoMonth.php @@ -0,0 +1,47 @@ +getMessage(); + } + $adjustmentMonths = floor($adjustmentMonths); + + // Execute function + $PHPDateObject = Helpers::adjustDateByMonths($dateValue, $adjustmentMonths + 1); + $adjustDays = (int) $PHPDateObject->format('d'); + $adjustDaysString = '-' . $adjustDays . ' days'; + $PHPDateObject->modify($adjustDaysString); + + return Helpers::returnIn3FormatsObject($PHPDateObject); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php new file mode 100644 index 00000000..48300642 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php @@ -0,0 +1,291 @@ +getMessage(); + } + } + + /** + * getTimeValue. + * + * @param string $timeValue + * + * @return mixed Excel date/time serial value, or string if error + */ + public static function getTimeValue($timeValue) + { + $saveReturnDateType = Functions::getReturnDateType(); + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + $timeValue = TimeValue::funcTimeValue($timeValue); + Functions::setReturnDateType($saveReturnDateType); + + return $timeValue; + } + + public static function adjustDateByMonths($dateValue = 0, $adjustmentMonths = 0) + { + // Execute function + $PHPDateObject = Date::excelToDateTimeObject($dateValue); + $oMonth = (int) $PHPDateObject->format('m'); + $oYear = (int) $PHPDateObject->format('Y'); + + $adjustmentMonthsString = (string) $adjustmentMonths; + if ($adjustmentMonths > 0) { + $adjustmentMonthsString = '+' . $adjustmentMonths; + } + if ($adjustmentMonths != 0) { + $PHPDateObject->modify($adjustmentMonthsString . ' months'); + } + $nMonth = (int) $PHPDateObject->format('m'); + $nYear = (int) $PHPDateObject->format('Y'); + + $monthDiff = ($nMonth - $oMonth) + (($nYear - $oYear) * 12); + if ($monthDiff != $adjustmentMonths) { + $adjustDays = (int) $PHPDateObject->format('d'); + $adjustDaysString = '-' . $adjustDays . ' days'; + $PHPDateObject->modify($adjustDaysString); + } + + return $PHPDateObject; + } + + /** + * Help reduce perceived complexity of some tests. + * + * @param mixed $value + * @param mixed $altValue + */ + public static function replaceIfEmpty(&$value, $altValue): void + { + $value = $value ?: $altValue; + } + + /** + * Adjust year in ambiguous situations. + */ + public static function adjustYear(string $testVal1, string $testVal2, string &$testVal3): void + { + if (!is_numeric($testVal1) || $testVal1 < 31) { + if (!is_numeric($testVal2) || $testVal2 < 12) { + if (is_numeric($testVal3) && $testVal3 < 12) { + $testVal3 += 2000; + } + } + } + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + public static function returnIn3FormatsArray(array $dateArray, bool $noFrac = false) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return new DateTime( + $dateArray['year'] + . '-' . $dateArray['month'] + . '-' . $dateArray['day'] + . ' ' . $dateArray['hour'] + . ':' . $dateArray['minute'] + . ':' . $dateArray['second'] + ); + } + $excelDateValue = + Date::formattedPHPToExcel( + $dateArray['year'], + $dateArray['month'], + $dateArray['day'], + $dateArray['hour'], + $dateArray['minute'], + $dateArray['second'] + ); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $noFrac ? floor($excelDateValue) : (float) $excelDateValue; + } + // RETURNDATE_UNIX_TIMESTAMP) + + return (int) Date::excelToTimestamp($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + public static function returnIn3FormatsFloat(float $excelDateValue) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + return $excelDateValue; + } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) Date::excelToTimestamp($excelDateValue); + } + // RETURNDATE_PHP_DATETIME_OBJECT + + return Date::excelToDateTimeObject($excelDateValue); + } + + /** + * Return result in one of three formats. + * + * @return mixed + */ + public static function returnIn3FormatsObject(DateTime $PHPDateObject) + { + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_PHP_DATETIME_OBJECT) { + return $PHPDateObject; + } + if ($retType === Functions::RETURNDATE_EXCEL) { + return (float) Date::PHPToExcel($PHPDateObject); + } + // RETURNDATE_UNIX_TIMESTAMP + + return (int) Date::excelToTimestamp(Date::PHPToExcel($PHPDateObject)); + } + + private static function baseDate(): int + { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + return 0; + } + if (Date::getExcelCalendar() === Date::CALENDAR_MAC_1904) { + return 0; + } + + return 1; + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @param mixed $number + */ + public static function nullFalseTrueToNumber(&$number, bool $allowBool = true): void + { + $number = Functions::flattenSingleValue($number); + $nullVal = self::baseDate(); + if ($number === null) { + $number = $nullVal; + } elseif ($allowBool && is_bool($number)) { + $number = $nullVal + (int) $number; + } + } + + /** + * Many functions accept null argument treated as 0. + * + * @param mixed $number + * + * @return float|int + */ + public static function validateNumericNull($number) + { + $number = Functions::flattenSingleValue($number); + if ($number === null) { + return 0; + } + if (is_numeric($number)) { + return $number; + } + + throw new Exception(Functions::VALUE()); + } + + /** + * Many functions accept null/false/true argument treated as 0/0/1. + * + * @param mixed $number + * + * @return float + */ + public static function validateNotNegative($number) + { + if (!is_numeric($number)) { + throw new Exception(Functions::VALUE()); + } + if ($number >= 0) { + return (float) $number; + } + + throw new Exception(Functions::NAN()); + } + + public static function silly1900(DateTime $PHPDateObject, string $mod = '-1 day'): void + { + $isoDate = $PHPDateObject->format('c'); + if ($isoDate < '1900-03-01') { + $PHPDateObject->modify($mod); + } + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Hour.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Hour.php new file mode 100644 index 00000000..98d4570d --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Hour.php @@ -0,0 +1,44 @@ +getMessage(); + } + + // Execute function + $timeValue = fmod($timeValue, 1); + $timeValue = Date::excelToDateTimeObject($timeValue); + + return (int) $timeValue->format('H'); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/IsoWeekNum.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/IsoWeekNum.php new file mode 100644 index 00000000..41959d9a --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/IsoWeekNum.php @@ -0,0 +1,55 @@ +getMessage(); + } + + // Execute function + $PHPDateObject = Date::excelToDateTimeObject($dateValue); + Helpers::silly1900($PHPDateObject); + + return (int) $PHPDateObject->format('W'); + } + + private static function apparentBug($dateValue): bool + { + if (Date::getExcelCalendar() !== DATE::CALENDAR_MAC_1904) { + if (is_bool($dateValue)) { + return true; + } + if (is_numeric($dateValue) && !((int) $dateValue)) { + return true; + } + } + + return false; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Minute.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Minute.php new file mode 100644 index 00000000..a1747ec9 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Minute.php @@ -0,0 +1,44 @@ +getMessage(); + } + + // Execute function + $timeValue = fmod($timeValue, 1); + $timeValue = Date::excelToDateTimeObject($timeValue); + + return (int) $timeValue->format('i'); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php new file mode 100644 index 00000000..a9fb8ece --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Month.php @@ -0,0 +1,40 @@ +getMessage(); + } + if ($dateValue < 1 && Date::getExcelCalendar() === DATE::CALENDAR_WINDOWS_1900) { + return 1; + } + + // Execute function + $PHPDateObject = Date::excelToDateTimeObject($dateValue); + + return (int) $PHPDateObject->format('n'); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php new file mode 100644 index 00000000..c700c834 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/NetworkDays.php @@ -0,0 +1,102 @@ +getMessage(); + } + + // Execute function + $startDow = self::calcStartDow($startDate); + $endDow = self::calcEndDow($endDate); + $wholeWeekDays = (int) floor(($endDate - $startDate) / 7) * 5; + $partWeekDays = self::calcPartWeekDays($startDow, $endDow); + + // Test any extra holiday parameters + $holidayCountedArray = []; + foreach ($holidayArray as $holidayDate) { + if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { + if ((WeekDay::funcWeekDay($holidayDate, 2) < 6) && (!in_array($holidayDate, $holidayCountedArray))) { + --$partWeekDays; + $holidayCountedArray[] = $holidayDate; + } + } + } + + return self::applySign($wholeWeekDays + $partWeekDays, $sDate, $eDate); + } + + private static function calcStartDow(float $startDate): int + { + $startDow = 6 - (int) WeekDay::funcWeekDay($startDate, 2); + if ($startDow < 0) { + $startDow = 5; + } + + return $startDow; + } + + private static function calcEndDow(float $endDate): int + { + $endDow = (int) WeekDay::funcWeekDay($endDate, 2); + if ($endDow >= 6) { + $endDow = 0; + } + + return $endDow; + } + + private static function calcPartWeekDays(int $startDow, int $endDow): int + { + $partWeekDays = $endDow + $startDow; + if ($partWeekDays > 5) { + $partWeekDays -= 5; + } + + return $partWeekDays; + } + + private static function applySign(int $result, float $sDate, float $eDate) + { + return ($sDate > $eDate) ? -$result : $result; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Now.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Now.php new file mode 100644 index 00000000..6e6bd171 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Now.php @@ -0,0 +1,34 @@ +format('c')); + + return is_array($dateArray) ? Helpers::returnIn3FormatsArray($dateArray) : Functions::VALUE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Second.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Second.php new file mode 100644 index 00000000..c4749993 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Second.php @@ -0,0 +1,44 @@ +getMessage(); + } + + // Execute function + $timeValue = fmod($timeValue, 1); + $timeValue = Date::excelToDateTimeObject($timeValue); + + return (int) $timeValue->format('s'); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php new file mode 100644 index 00000000..450f9d50 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Time.php @@ -0,0 +1,116 @@ +getMessage(); + } + + self::adjustSecond($second, $minute); + self::adjustMinute($minute, $hour); + + if ($hour > 23) { + $hour = $hour % 24; + } elseif ($hour < 0) { + return Functions::NAN(); + } + + // Execute function + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $calendar = Date::getExcelCalendar(); + $date = (int) ($calendar !== Date::CALENDAR_WINDOWS_1900); + + return (float) Date::formattedPHPToExcel($calendar, 1, $date, $hour, $minute, $second); + } + if ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + return (int) Date::excelToTimestamp(Date::formattedPHPToExcel(1970, 1, 1, $hour, $minute, $second)); // -2147468400; // -2147472000 + 3600 + } + // RETURNDATE_PHP_DATETIME_OBJECT + // Hour has already been normalized (0-23) above + $phpDateObject = new DateTime('1900-01-01 ' . $hour . ':' . $minute . ':' . $second); + + return $phpDateObject; + } + + private static function adjustSecond(int &$second, int &$minute): void + { + if ($second < 0) { + $minute += floor($second / 60); + $second = 60 - abs($second % 60); + if ($second == 60) { + $second = 0; + } + } elseif ($second >= 60) { + $minute += floor($second / 60); + $second = $second % 60; + } + } + + private static function adjustMinute(int &$minute, int &$hour): void + { + if ($minute < 0) { + $hour += floor($minute / 60); + $minute = 60 - abs($minute % 60); + if ($minute == 60) { + $minute = 0; + } + } elseif ($minute >= 60) { + $hour += floor($minute / 60); + $minute = $minute % 60; + } + } + + 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()); + } + + return (int) $value; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php new file mode 100644 index 00000000..2366b1d6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/TimeValue.php @@ -0,0 +1,61 @@ + 24) { + $arraySplit[0] = ($arraySplit[0] % 24); + $timeValue = implode(':', $arraySplit); + } + + $PHPDateArray = date_parse($timeValue); + $retValue = Functions::VALUE(); + if (($PHPDateArray !== false) && ($PHPDateArray['error_count'] == 0)) { + // OpenOffice-specific code removed - it works just like Excel + $excelDateValue = Date::formattedPHPToExcel(1900, 1, 1, $PHPDateArray['hour'], $PHPDateArray['minute'], $PHPDateArray['second']) - 1; + + $retType = Functions::getReturnDateType(); + if ($retType === Functions::RETURNDATE_EXCEL) { + $retValue = (float) $excelDateValue; + } elseif ($retType === Functions::RETURNDATE_UNIX_TIMESTAMP) { + $retValue = (int) $phpDateValue = Date::excelToTimestamp($excelDateValue + 25569) - 3600; + } else { + $retValue = new DateTime('1900-01-01 ' . $PHPDateArray['hour'] . ':' . $PHPDateArray['minute'] . ':' . $PHPDateArray['second']); + } + } + + return $retValue; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Today.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Today.php new file mode 100644 index 00000000..5e459410 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Today.php @@ -0,0 +1,34 @@ +format('c')); + + return is_array($dateArray) ? Helpers::returnIn3FormatsArray($dateArray, true) : Functions::VALUE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekDay.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekDay.php new file mode 100644 index 00000000..15811ee5 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekDay.php @@ -0,0 +1,80 @@ +getMessage(); + } + + // Execute function + $PHPDateObject = Date::excelToDateTimeObject($dateValue); + Helpers::silly1900($PHPDateObject); + $DoW = (int) $PHPDateObject->format('w'); + + switch ($style) { + case 1: + ++$DoW; + + break; + case 2: + $DoW = self::dow0Becomes7($DoW); + + break; + case 3: + $DoW = self::dow0Becomes7($DoW) - 1; + + break; + } + + return $DoW; + } + + private static function validateStyle($style): int + { + $style = Functions::flattenSingleValue($style); + + if (!is_numeric($style)) { + throw new Exception(Functions::VALUE()); + } + $style = (int) $style; + if (($style < 1) || ($style > 3)) { + throw new Exception(Functions::NAN()); + } + + return $style; + } + + private static function dow0Becomes7(int $DoW): int + { + return ($DoW === 0) ? 7 : $DoW; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php new file mode 100644 index 00000000..1dd15edb --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php @@ -0,0 +1,130 @@ +getMessage(); + } + + // Execute function + $PHPDateObject = Date::excelToDateTimeObject($dateValue); + if ($method == Constants::STARTWEEK_MONDAY_ISO) { + Helpers::silly1900($PHPDateObject); + + return (int) $PHPDateObject->format('W'); + } + if (self::buggyWeekNum1904($method, $origDateValueNull, $PHPDateObject)) { + return 0; + } + Helpers::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches + $dayOfYear = $PHPDateObject->format('z'); + $PHPDateObject->modify('-' . $dayOfYear . ' days'); + $firstDayOfFirstWeek = $PHPDateObject->format('w'); + $daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; + $daysInFirstWeek += 7 * !$daysInFirstWeek; + $endFirstWeek = $daysInFirstWeek - 1; + $weekOfYear = floor(($dayOfYear - $endFirstWeek + 13) / 7); + + return (int) $weekOfYear; + } + + /** + * Validate dateValue parameter. + * + * @param mixed $dateValue + */ + private static function validateDateValue($dateValue): float + { + if (is_bool($dateValue)) { + throw new Exception(Functions::VALUE()); + } + + return Helpers::getDateValue($dateValue); + } + + /** + * Validate method parameter. + * + * @param mixed $method + */ + private static function validateMethod($method): int + { + if ($method === null) { + $method = Constants::STARTWEEK_SUNDAY; + } + $method = Functions::flattenSingleValue($method); + if (!is_numeric($method)) { + throw new Exception(Functions::VALUE()); + } + + $method = (int) $method; + if (!array_key_exists($method, Constants::METHODARR)) { + throw new Exception(Functions::NAN()); + } + $method = Constants::METHODARR[$method]; + + return $method; + } + + private static function buggyWeekNum1900(int $method): bool + { + return $method === Constants::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_WINDOWS_1900; + } + + private static function buggyWeekNum1904(int $method, bool $origNull, DateTime $dateObject): bool + { + // This appears to be another Excel bug. + + return $method === Constants::DOW_SUNDAY && Date::getExcelCalendar() === Date::CALENDAR_MAC_1904 && !$origNull && $dateObject->format('Y-m-d') === '1904-01-01'; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php new file mode 100644 index 00000000..f812624e --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php @@ -0,0 +1,182 @@ +getMessage(); + } + + $startDate = (float) floor($startDate); + $endDays = (int) floor($endDays); + // If endDays is 0, we always return startDate + if ($endDays == 0) { + return $startDate; + } + if ($endDays < 0) { + return self::decrementing($startDate, $endDays, $holidayArray); + } + + return self::incrementing($startDate, $endDays, $holidayArray); + } + + /** + * Use incrementing logic to determine Workday. + * + * @return mixed + */ + private static function incrementing(float $startDate, int $endDays, array $holidayArray) + { + // Adjust the start date if it falls over a weekend + + $startDoW = WeekDay::funcWeekDay($startDate, 3); + if (WeekDay::funcWeekDay($startDate, 3) >= 5) { + $startDate += 7 - $startDoW; + --$endDays; + } + + // Add endDays + $endDate = (float) $startDate + ((int) ($endDays / 5) * 7); + $endDays = $endDays % 5; + while ($endDays > 0) { + ++$endDate; + // Adjust the calculated end date if it falls over a weekend + $endDow = WeekDay::funcWeekDay($endDate, 3); + if ($endDow >= 5) { + $endDate += 7 - $endDow; + } + --$endDays; + } + + // Test any extra holiday parameters + if (!empty($holidayArray)) { + $endDate = self::incrementingArray($startDate, $endDate, $holidayArray); + } + + return Helpers::returnIn3FormatsFloat($endDate); + } + + private static function incrementingArray(float $startDate, float $endDate, array $holidayArray): float + { + $holidayCountedArray = $holidayDates = []; + foreach ($holidayArray as $holidayDate) { + if (WeekDay::funcWeekDay($holidayDate, 3) < 5) { + $holidayDates[] = $holidayDate; + } + } + sort($holidayDates, SORT_NUMERIC); + foreach ($holidayDates as $holidayDate) { + if (($holidayDate >= $startDate) && ($holidayDate <= $endDate)) { + if (!in_array($holidayDate, $holidayCountedArray)) { + ++$endDate; + $holidayCountedArray[] = $holidayDate; + } + } + // Adjust the calculated end date if it falls over a weekend + $endDoW = WeekDay::funcWeekDay($endDate, 3); + if ($endDoW >= 5) { + $endDate += 7 - $endDoW; + } + } + + return $endDate; + } + + /** + * Use decrementing logic to determine Workday. + * + * @return mixed + */ + private static function decrementing(float $startDate, int $endDays, array $holidayArray) + { + // Adjust the start date if it falls over a weekend + + $startDoW = WeekDay::funcWeekDay($startDate, 3); + if (WeekDay::funcWeekDay($startDate, 3) >= 5) { + $startDate += -$startDoW + 4; + ++$endDays; + } + + // Add endDays + $endDate = (float) $startDate + ((int) ($endDays / 5) * 7); + $endDays = $endDays % 5; + while ($endDays < 0) { + --$endDate; + // Adjust the calculated end date if it falls over a weekend + $endDow = WeekDay::funcWeekDay($endDate, 3); + if ($endDow >= 5) { + $endDate += 4 - $endDow; + } + ++$endDays; + } + + // Test any extra holiday parameters + if (!empty($holidayArray)) { + $endDate = self::decrementingArray($startDate, $endDate, $holidayArray); + } + + return Helpers::returnIn3FormatsFloat($endDate); + } + + private static function decrementingArray(float $startDate, float $endDate, array $holidayArray): float + { + $holidayCountedArray = $holidayDates = []; + foreach ($holidayArray as $holidayDate) { + if (WeekDay::funcWeekDay($holidayDate, 3) < 5) { + $holidayDates[] = $holidayDate; + } + } + rsort($holidayDates, SORT_NUMERIC); + foreach ($holidayDates as $holidayDate) { + if (($holidayDate <= $startDate) && ($holidayDate >= $endDate)) { + if (!in_array($holidayDate, $holidayCountedArray)) { + --$endDate; + $holidayCountedArray[] = $holidayDate; + } + } + // Adjust the calculated end date if it falls over a weekend + $endDoW = WeekDay::funcWeekDay($endDate, 3); + if ($endDoW >= 5) { + $endDate += -$endDoW + 4; + } + } + + return $endDate; + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Year.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Year.php new file mode 100644 index 00000000..5fcac739 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Year.php @@ -0,0 +1,40 @@ +getMessage(); + } + + if ($dateValue < 1 && Date::getExcelCalendar() === DATE::CALENDAR_WINDOWS_1900) { + return 1900; + } + // Execute function + $PHPDateObject = Date::excelToDateTimeObject($dateValue); + + return (int) $PHPDateObject->format('Y'); + } +} diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php new file mode 100644 index 00000000..a99b1c7f --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/YearFrac.php @@ -0,0 +1,120 @@ +getMessage(); + } + + switch ($method) { + case 0: + return Days360::funcDays360($startDate, $endDate) / 360; + case 1: + return self::method1($startDate, $endDate); + case 2: + return DateDif::funcDateDif($startDate, $endDate) / 360; + case 3: + return DateDif::funcDateDif($startDate, $endDate) / 365; + case 4: + return Days360::funcDays360($startDate, $endDate, true) / 360; + } + + return Functions::NAN(); + } + + /** + * Excel 1900 calendar treats date argument of null as 1900-01-00. Really. + * + * @param mixed $startDate + * @param mixed $endDate + */ + private static function excelBug(float $sDate, $startDate, $endDate, int $method): float + { + if (Functions::getCompatibilityMode() !== Functions::COMPATIBILITY_OPENOFFICE && Date::getExcelCalendar() !== Date::CALENDAR_MAC_1904) { + if ($endDate === null && $startDate !== null) { + if (Month::funcMonth($sDate) == 12 && Day::funcDay($sDate) === 31 && $method === 0) { + $sDate += 2; + } else { + ++$sDate; + } + } + } + + return $sDate; + } + + private static function method1(float $startDate, float $endDate): float + { + $days = DateDif::funcDateDif($startDate, $endDate); + $startYear = Year::funcYear($startDate); + $endYear = Year::funcYear($endDate); + $years = $endYear - $startYear + 1; + $startMonth = Month::funcMonth($startDate); + $startDay = Day::funcDay($startDate); + $endMonth = Month::funcMonth($endDate); + $endDay = Day::funcDay($endDate); + $startMonthDay = 100 * $startMonth + $startDay; + $endMonthDay = 100 * $endMonth + $endDay; + if ($years == 1) { + $tmpCalcAnnualBasis = 365 + (int) Helpers::isLeapYear($endYear); + } elseif ($years == 2 && $startMonthDay >= $endMonthDay) { + if (Helpers::isLeapYear($startYear)) { + $tmpCalcAnnualBasis = 365 + (int) ($startMonthDay <= 229); + } elseif (Helpers::isLeapYear($endYear)) { + $tmpCalcAnnualBasis = 365 + (int) ($endMonthDay >= 229); + } else { + $tmpCalcAnnualBasis = 365; + } + } else { + $tmpCalcAnnualBasis = 0; + for ($year = $startYear; $year <= $endYear; ++$year) { + $tmpCalcAnnualBasis += 365 + (int) Helpers::isLeapYear($year); + } + $tmpCalcAnnualBasis /= $years; + } + + return $days / $tmpCalcAnnualBasis; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php new file mode 100644 index 00000000..c56c7431 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/AllSetupTeardown.php @@ -0,0 +1,71 @@ +compatibilityMode = Functions::getCompatibilityMode(); + $this->excelCalendar = Date::getExcelCalendar(); + $this->returnDateType = Functions::getReturnDateType(); + $this->spreadsheet = new Spreadsheet(); + $this->sheet = $this->spreadsheet->getActiveSheet(); + } + + protected function tearDown(): void + { + Date::setExcelCalendar($this->excelCalendar); + Functions::setCompatibilityMode($this->compatibilityMode); + Functions::setReturnDateType($this->returnDateType); + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + $this->sheet = null; + } + + protected static function setMac1904(): void + { + Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + } + + protected static function setUnixReturn(): void + { + Functions::setReturnDateType(Functions::RETURNDATE_UNIX_TIMESTAMP); + } + + protected static function setObjectReturn(): void + { + Functions::setReturnDateType(Functions::RETURNDATE_PHP_DATETIME_OBJECT); + } + + protected static function setOpenOffice(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + } + + /** + * @param mixed $expectedResult + */ + protected function mightHaveException($expectedResult): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcException::class); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateDifTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateDifTest.php index db8e29a1..6c394087 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateDifTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateDifTest.php @@ -2,32 +2,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class DateDifTest extends TestCase +class DateDifTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerDATEDIF * * @param mixed $expectedResult - * @param $startDate - * @param $endDate - * @param $unit */ - public function testDATEDIF($expectedResult, $startDate, $endDate, $unit): void + public function testDATEDIF($expectedResult, string $formula): void { - $result = DateTime::DATEDIF($startDate, $endDate, $unit); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('1954-11-23'); + $sheet->getCell('A1')->setValue("=DATEDIF($formula)"); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerDATEDIF() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php index aad59729..354e6f3b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateTest.php @@ -2,42 +2,22 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Datefunc; -class DateTest extends TestCase +class DateTest extends AllSetupTeardown { - private $returnDateType; - - private $excelCalendar; - - protected function setUp(): void - { - $this->returnDateType = Functions::getReturnDateType(); - $this->excelCalendar = Date::getExcelCalendar(); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - } - - protected function tearDown(): void - { - Functions::setReturnDateType($this->returnDateType); - Date::setExcelCalendar($this->excelCalendar); - } - /** * @dataProvider providerDATE * * @param mixed $expectedResult - * @param $year - * @param $month - * @param $day */ - public function testDATE($expectedResult, $year, $month, $day): void + public function testDATE($expectedResult, string $formula): void { - $result = DateTime::DATE($year, $month, $day); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('1954-11-23'); + $sheet->getCell('A1')->setValue("=DATE($formula)"); + self::assertEquals($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerDATE() @@ -47,18 +27,17 @@ class DateTest extends TestCase public function testDATEtoUnixTimestamp(): void { - Functions::setReturnDateType(Functions::RETURNDATE_UNIX_TIMESTAMP); + self::setUnixReturn(); - $result = DateTime::DATE(2012, 1, 31); + $result = Datefunc::funcDate(2012, 1, 31); // 32-bit safe self::assertEquals(1327968000, $result); - self::assertEqualsWithDelta(1327968000, $result, 1E-8); } public function testDATEtoDateTimeObject(): void { - Functions::setReturnDateType(Functions::RETURNDATE_PHP_DATETIME_OBJECT); + self::setObjectReturn(); - $result = DateTime::DATE(2012, 1, 31); + $result = Datefunc::funcDate(2012, 1, 31); // Must return an object... self::assertIsObject($result); // ... of the correct type @@ -69,17 +48,12 @@ class DateTest extends TestCase public function testDATEwith1904Calendar(): void { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); + self::setMac1904(); - $result = DateTime::DATE(1918, 11, 11); + $result = Datefunc::funcDate(1918, 11, 11); self::assertEquals($result, 5428); - } - public function testDATEwith1904CalendarError(): void - { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); - - $result = DateTime::DATE(1901, 1, 31); + $result = Datefunc::funcDate(1901, 1, 31); self::assertEquals($result, '#NUM!'); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php index 72e036f9..fc432bbe 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DateValueTest.php @@ -4,50 +4,33 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; use DateTimeImmutable; use DateTimeInterface; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\DateValue; -class DateValueTest extends TestCase +class DateValueTest extends AllSetupTeardown { - private $returnDateType; - - private $excelCalendar; - - protected function setUp(): void - { - $this->returnDateType = Functions::getReturnDateType(); - $this->excelCalendar = Date::getExcelCalendar(); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - } - - protected function tearDown(): void - { - Functions::setReturnDateType($this->returnDateType); - Date::setExcelCalendar($this->excelCalendar); - } - /** * @dataProvider providerDATEVALUE * * @param mixed $expectedResult - * @param $dateValue */ - public function testDATEVALUE($expectedResult, $dateValue): void + public function testDATEVALUE($expectedResult, string $dateValue): void { + $this->sheet->getCell('B1')->setValue('1954-07-20'); // Loop to avoid extraordinarily rare edge case where first calculation // and second do not take place on same day. + $row = 0; do { + ++$row; $dtStart = new DateTimeImmutable(); $startDay = $dtStart->format('d'); if (is_string($expectedResult)) { $replYMD = str_replace('Y', date('Y'), $expectedResult); if ($replYMD !== $expectedResult) { - $expectedResult = DateTime::DATEVALUE($replYMD); + $expectedResult = DateValue::funcDateValue($replYMD); } } - $result = DateTime::DATEVALUE($dateValue); + $this->sheet->getCell("A$row")->setValue("=DATEVALUE($dateValue)"); + $result = $this->sheet->getCell("A$row")->getCalculatedValue(); $dtEnd = new DateTimeImmutable(); $endDay = $dtEnd->format('d'); } while ($startDay !== $endDay); @@ -61,18 +44,18 @@ class DateValueTest extends TestCase public function testDATEVALUEtoUnixTimestamp(): void { - Functions::setReturnDateType(Functions::RETURNDATE_UNIX_TIMESTAMP); + self::setUnixReturn(); - $result = DateTime::DATEVALUE('2012-1-31'); + $result = DateValue::funcDateValue('2012-1-31'); self::assertEquals(1327968000, $result); self::assertEqualsWithDelta(1327968000, $result, 1E-8); } public function testDATEVALUEtoDateTimeObject(): void { - Functions::setReturnDateType(Functions::RETURNDATE_PHP_DATETIME_OBJECT); + self::setObjectReturn(); - $result = DateTime::DATEVALUE('2012-1-31'); + $result = DateValue::funcDateValue('2012-1-31'); // Must return an object... self::assertIsObject($result); // ... of the correct type @@ -83,10 +66,10 @@ class DateValueTest extends TestCase public function testDATEVALUEwith1904Calendar(): void { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); - self::assertEquals(5428, DateTime::DATEVALUE('1918-11-11')); - self::assertEquals(0, DateTime::DATEVALUE('1904-01-01')); - self::assertEquals('#VALUE!', DateTime::DATEVALUE('1903-12-31')); - self::assertEquals('#VALUE!', DateTime::DATEVALUE('1900-02-29')); + self::setMac1904(); + self::assertEquals(5428, DateValue::funcDateValue('1918-11-11')); + self::assertEquals(0, DateValue::funcDateValue('1904-01-01')); + self::assertEquals('#VALUE!', DateValue::funcDateValue('1903-12-31')); + self::assertEquals('#VALUE!', DateValue::funcDateValue('1900-02-29')); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DayTest.php index 482e068d..e50475cf 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DayTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DayTest.php @@ -2,56 +2,43 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class DayTest extends TestCase +class DayTest extends AllSetupTeardown { - private $compatibilityMode; - - private $returnDateType; - - private $excelCalendar; - - protected function setUp(): void - { - $this->compatibilityMode = Functions::getCompatibilityMode(); - $this->returnDateType = Functions::getReturnDateType(); - $this->excelCalendar = Date::getExcelCalendar(); - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - - protected function tearDown(): void - { - Functions::setCompatibilityMode($this->compatibilityMode); - Functions::setReturnDateType($this->returnDateType); - Date::setExcelCalendar($this->excelCalendar); - } - /** * @dataProvider providerDAY * * @param mixed $expectedResultExcel - * @param mixed $expectedResultOpenOffice - * @param $dateTimeValue */ - public function testDAY($expectedResultExcel, $expectedResultOpenOffice, $dateTimeValue): void + public function testDAY($expectedResultExcel, string $dateTimeValue): void { - $resultExcel = DateTime::DAYOFMONTH($dateTimeValue); - self::assertEqualsWithDelta($expectedResultExcel, $resultExcel, 1E-8); - - Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); - - $resultOpenOffice = DateTime::DAYOFMONTH($dateTimeValue); - self::assertEqualsWithDelta($expectedResultOpenOffice, $resultOpenOffice, 1E-8); + $this->mightHaveException($expectedResultExcel); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('1954-11-23'); + $sheet->getCell('A1')->setValue("=DAY($dateTimeValue)"); + self::assertSame($expectedResultExcel, $sheet->getCell('A1')->getCalculatedValue()); } public function providerDAY() { return require 'tests/data/Calculation/DateTime/DAY.php'; } + + /** + * @dataProvider providerDAYOpenOffice + * + * @param mixed $expectedResultOpenOffice + */ + public function testDAYOpenOffice($expectedResultOpenOffice, string $dateTimeValue): void + { + self::setOpenOffice(); + $this->mightHaveException($expectedResultOpenOffice); + $sheet = $this->sheet; + $sheet->getCell('A2')->setValue("=DAY($dateTimeValue)"); + self::assertSame($expectedResultOpenOffice, $sheet->getCell('A2')->getCalculatedValue()); + } + + public function providerDAYOpenOffice() + { + return require 'tests/data/Calculation/DateTime/DAYOpenOffice.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/Days360Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/Days360Test.php index 47449e0d..5d6ba29e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/Days360Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/Days360Test.php @@ -2,32 +2,21 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class Days360Test extends TestCase +class Days360Test extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerDAYS360 * * @param mixed $expectedResult - * @param $startDate - * @param $endDate - * @param $method */ - public function testDAYS360($expectedResult, $startDate, $endDate, $method): void + public function testDAYS360($expectedResult, string $formula): void { - $result = DateTime::DAYS360($startDate, $endDate, $method); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('2000-02-29'); + $sheet->getCell('C1')->setValue('2000-03-31'); + $sheet->getCell('A1')->setValue("=DAYS360($formula)"); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerDAYS360() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DaysTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DaysTest.php index fe31dfcc..8b3ea392 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DaysTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/DaysTest.php @@ -2,35 +2,44 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use DateTime; +use DateTimeImmutable; +use Exception; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Days; -class DaysTest extends TestCase +class DaysTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerDAYS * * @param mixed $expectedResult - * @param $endDate - * @param $startDate */ - public function testDAYS($expectedResult, $endDate, $startDate): void + public function testDAYS($expectedResult, string $formula): void { - $result = DateTime::DAYS($endDate, $startDate); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('1954-11-23'); + $sheet->getCell('C1')->setValue('1954-11-30'); + $sheet->getCell('A1')->setValue("=DAYS($formula)"); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerDAYS() { return require 'tests/data/Calculation/DateTime/DAYS.php'; } + + public function testObject(): void + { + $obj1 = new DateTime('2000-3-31'); + $obj2 = new DateTimeImmutable('2000-2-29'); + self::assertSame(31, Days::funcDays($obj1, $obj2)); + } + + public function testNonDateObject(): void + { + $obj1 = new Exception(); + $obj2 = new DateTimeImmutable('2000-2-29'); + self::assertSame('#VALUE!', Days::funcDays($obj1, $obj2)); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EDateTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EDateTest.php index a887ba5b..384e1aec 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EDateTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EDateTest.php @@ -2,31 +2,22 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\EDate; -class EDateTest extends TestCase +class EDateTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerEDATE * * @param mixed $expectedResult - * @param $dateValue - * @param $adjustmentMonths */ - public function testEDATE($expectedResult, $dateValue, $adjustmentMonths): void + public function testEDATE($expectedResult, string $formula): void { - $result = DateTime::EDATE($dateValue, $adjustmentMonths); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=EDATE($formula)"); + $sheet->getCell('B1')->setValue('1954-11-23'); + self::assertEquals($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerEDATE() @@ -36,18 +27,18 @@ class EDateTest extends TestCase public function testEDATEtoUnixTimestamp(): void { - Functions::setReturnDateType(Functions::RETURNDATE_UNIX_TIMESTAMP); + self::setUnixReturn(); - $result = DateTime::EDATE('2012-1-26', -1); + $result = EDate::funcEDate('2012-1-26', -1); self::assertEquals(1324857600, $result); self::assertEqualsWithDelta(1324857600, $result, 1E-8); } public function testEDATEtoDateTimeObject(): void { - Functions::setReturnDateType(Functions::RETURNDATE_PHP_DATETIME_OBJECT); + self::setObjectReturn(); - $result = DateTime::EDATE('2012-1-26', -1); + $result = EDate::funcEDate('2012-1-26', -1); // Must return an object... self::assertIsObject($result); // ... of the correct type diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EoMonthTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EoMonthTest.php index f9c54039..1af81c9f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EoMonthTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/EoMonthTest.php @@ -2,31 +2,22 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\EoMonth; -class EoMonthTest extends TestCase +class EoMonthTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerEOMONTH * * @param mixed $expectedResult - * @param $dateValue - * @param $adjustmentMonths */ - public function testEOMONTH($expectedResult, $dateValue, $adjustmentMonths): void + public function testEOMONTH($expectedResult, string $formula): void { - $result = DateTime::EOMONTH($dateValue, $adjustmentMonths); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=EOMONTH($formula)"); + $sheet->getCell('B1')->setValue('1954-11-23'); + self::assertEquals($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerEOMONTH() @@ -36,23 +27,22 @@ class EoMonthTest extends TestCase public function testEOMONTHtoUnixTimestamp(): void { - Functions::setReturnDateType(Functions::RETURNDATE_UNIX_TIMESTAMP); + self::setUnixReturn(); - $result = DateTime::EOMONTH('2012-1-26', -1); + $result = EoMonth::funcEomonth('2012-1-26', -1); self::assertEquals(1325289600, $result); - self::assertEqualsWithDelta(1325289600, $result, 1E-8); } public function testEOMONTHtoDateTimeObject(): void { - Functions::setReturnDateType(Functions::RETURNDATE_PHP_DATETIME_OBJECT); + self::setObjectReturn(); - $result = DateTime::EOMONTH('2012-1-26', -1); + $result = EoMonth::funcEomonth('2012-1-26', -1); // Must return an object... self::assertIsObject($result); // ... of the correct type self::assertTrue(is_a($result, 'DateTimeInterface')); // ... with the correct value - self::assertEquals($result->format('d-M-Y'), '31-Dec-2011'); + self::assertSame($result->format('d-M-Y'), '31-Dec-2011'); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/HourTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/HourTest.php index 2d0cd5d1..99544b5a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/HourTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/HourTest.php @@ -2,30 +2,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class HourTest extends TestCase +class HourTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerHOUR * * @param mixed $expectedResult - * @param $dateTimeValue */ - public function testHOUR($expectedResult, $dateTimeValue): void + public function testHOUR($expectedResult, string $dateTimeValue): void { - $result = DateTime::HOUROFDAY($dateTimeValue); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=HOUR($dateTimeValue)"); + $sheet->getCell('B1')->setValue('1954-11-23 2:23:46'); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerHOUR() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php index 1ef0080a..b27ca7d5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/IsoWeekNumTest.php @@ -2,34 +2,46 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class IsoWeekNumTest extends TestCase +class IsoWeekNumTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerISOWEEKNUM * * @param mixed $expectedResult - * @param mixed $dateValue + * @param string $dateValue */ public function testISOWEEKNUM($expectedResult, $dateValue): void { - $result = DateTime::ISOWEEKNUM($dateValue); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=ISOWEEKNUM($dateValue)"); + $sheet->getCell('B1')->setValue('1954-11-23'); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerISOWEEKNUM() { return require 'tests/data/Calculation/DateTime/ISOWEEKNUM.php'; } + + /** + * @dataProvider providerISOWEEKNUM1904 + * + * @param mixed $expectedResult + * @param string $dateValue + */ + public function testISOWEEKNUM1904($expectedResult, $dateValue): void + { + $this->mightHaveException($expectedResult); + self::setMac1904(); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=ISOWEEKNUM($dateValue)"); + $sheet->getCell('B1')->setValue('1954-11-23'); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); + } + + public function providerISOWEEKNUM1904() + { + return require 'tests/data/Calculation/DateTime/ISOWEEKNUM1904.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MinuteTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MinuteTest.php index 8472c6de..cbc2a1a4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MinuteTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MinuteTest.php @@ -2,30 +2,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class MinuteTest extends TestCase +class MinuteTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerMINUTE * * @param mixed $expectedResult - * @param $dateTimeValue */ - public function testMINUTE($expectedResult, $dateTimeValue): void + public function testMINUTE($expectedResult, string $dateTimeValue): void { - $result = DateTime::MINUTE($dateTimeValue); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=MINUTE($dateTimeValue)"); + $sheet->getCell('B1')->setValue('1954-11-23 2:23:46'); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerMINUTE() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MonthTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MonthTest.php index 62513702..a9f70229 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MonthTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MonthTest.php @@ -2,30 +2,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class MonthTest extends TestCase +class MonthTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerMONTH * * @param mixed $expectedResult - * @param $dateTimeValue */ - public function testMONTH($expectedResult, $dateTimeValue): void + public function testMONTH($expectedResult, string $dateTimeValue): void { - $result = DateTime::MONTHOFYEAR($dateTimeValue); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=MONTH($dateTimeValue)"); + $sheet->getCell('B1')->setValue('1954-11-23'); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerMONTH() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MovedFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MovedFunctionsTest.php new file mode 100644 index 00000000..d14f7d7d --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/MovedFunctionsTest.php @@ -0,0 +1,63 @@ +format('s'); + $nowResult = DateTime::DATETIMENOW(); + $todayResult = DateTime::DATENOW(); + $dtEnd = new DateTimeImmutable(); + $endSecond = $dtEnd->format('s'); + } while ($startSecond !== $endSecond); + self::assertSame(DateTime::DAYOFMONTH($nowResult), DateTime::DAYOFMONTH($todayResult)); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NetworkDaysTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NetworkDaysTest.php index e366c44e..568c661c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NetworkDaysTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NetworkDaysTest.php @@ -2,29 +2,47 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class NetworkDaysTest extends TestCase +class NetworkDaysTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerNETWORKDAYS * * @param mixed $expectedResult + * @param mixed $arg1 + * @param mixed $arg2 */ - public function testNETWORKDAYS($expectedResult, ...$args): void + public function testNETWORKDAYS($expectedResult, $arg1 = 'omitted', $arg2 = 'omitted', ?array $arg3 = null): void { - $result = DateTime::NETWORKDAYS(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('A1')->setValue($arg1); + } + if ($arg2 !== null) { + $sheet->getCell('A2')->setValue($arg2); + } + $dateArray = []; + if (is_array($arg3)) { + if (array_key_exists(0, $arg3) && is_array($arg3[0])) { + $dateArray = $arg3[0]; + } else { + $dateArray = $arg3; + } + } + $dateIndex = 0; + foreach ($dateArray as $date) { + ++$dateIndex; + $sheet->getCell("C$dateIndex")->setValue($date); + } + $arrayArg = $dateIndex ? ", C1:C$dateIndex" : ''; + if ($arg1 === 'omitted') { + $sheet->getCell('B1')->setValue('=NETWORKDAYS()'); + } elseif ($arg2 === 'omitted') { + $sheet->getCell('B1')->setValue('=NETWORKDAYS(A1)'); + } else { + $sheet->getCell('B1')->setValue("=NETWORKDAYS(A1, A2$arrayArg)"); + } + self::assertEquals($expectedResult, $sheet->getCell('B1')->getCalculatedValue()); } public function providerNETWORKDAYS() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php index f139f703..e0f68c24 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/NowTest.php @@ -3,15 +3,12 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; use DateTimeImmutable; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; -class NowTest extends TestCase +class NowTest extends AllSetupTeardown { public function testNow(): void { - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $sheet = $this->sheet; // Loop to avoid rare edge case where first calculation // and second do not take place in same second. do { @@ -21,7 +18,6 @@ class NowTest extends TestCase $dtEnd = new DateTimeImmutable(); $endSecond = $dtEnd->format('s'); } while ($startSecond !== $endSecond); - //echo("\n"); var_dump($sheet->getCell('A1')->getCalculatedValue()); echo ("\n"); $sheet->setCellValue('B1', '=YEAR(A1)'); $sheet->setCellValue('C1', '=MONTH(A1)'); $sheet->setCellValue('D1', '=DAY(A1)'); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/SecondTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/SecondTest.php index bc2b0752..03cef8bc 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/SecondTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/SecondTest.php @@ -2,30 +2,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class SecondTest extends TestCase +class SecondTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerSECOND * * @param mixed $expectedResult - * @param $dateTimeValue */ - public function testSECOND($expectedResult, $dateTimeValue): void + public function testSECOND($expectedResult, string $dateTimeValue): void { - $result = DateTime::SECOND($dateTimeValue); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=SECOND($dateTimeValue)"); + $sheet->getCell('B1')->setValue('1954-11-23 2:23:46'); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerSECOND() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php index 3ef58374..f33b5aac 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeTest.php @@ -2,39 +2,24 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Time; -class TimeTest extends TestCase +class TimeTest extends AllSetupTeardown { - private $returnDateType; - - private $calendar; - - protected function setUp(): void - { - $this->returnDateType = Functions::getReturnDateType(); - $this->calendar = Date::getExcelCalendar(); - } - - protected function tearDown(): void - { - Functions::setReturnDateType($this->returnDateType); - Date::setExcelCalendar($this->calendar); - } - /** * @dataProvider providerTIME * * @param mixed $expectedResult */ - public function testTIME($expectedResult, ...$args): void + public function testTIME($expectedResult, string $formula): void { - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - $result = DateTime::TIME(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('15'); + $sheet->getCell('B2')->setValue('32'); + $sheet->getCell('B3')->setValue('50'); + $sheet->getCell('A1')->setValue("=TIME($formula)"); + self::assertEqualsWithDelta($expectedResult, $sheet->getCell('A1')->getCalculatedValue(), 1E-8); } public function providerTIME() @@ -44,17 +29,17 @@ class TimeTest extends TestCase public function testTIMEtoUnixTimestamp(): void { - Functions::setReturnDateType(Functions::RETURNDATE_PHP_NUMERIC); + self::setUnixReturn(); - $result = DateTime::TIME(7, 30, 20); + $result = Time::funcTime(7, 30, 20); self::assertEqualsWithDelta(27020, $result, 1E-8); } public function testTIMEtoDateTimeObject(): void { - Functions::setReturnDateType(Functions::RETURNDATE_PHP_OBJECT); + self::setObjectReturn(); - $result = DateTime::TIME(7, 30, 20); + $result = Time::funcTime(7, 30, 20); // Must return an object... self::assertIsObject($result); // ... of the correct type @@ -65,17 +50,14 @@ class TimeTest extends TestCase public function testTIME1904(): void { - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); - $result = DateTime::TIME(0, 0, 0); + self::setMac1904(); + $result = Time::funcTime(0, 0, 0); self::assertEquals(0, $result); } public function testTIME1900(): void { - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - $result = DateTime::TIME(0, 0, 0); + $result = Time::funcTime(0, 0, 0); self::assertEquals(0, $result); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php index 04b8c058..f144c6f2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php @@ -2,20 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\TimeValue; -class TimeValueTest extends TestCase +class TimeValueTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerTIMEVALUE * @@ -24,7 +14,11 @@ class TimeValueTest extends TestCase */ public function testTIMEVALUE($expectedResult, $timeValue): void { - $result = DateTime::TIMEVALUE($timeValue); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('03:45:52'); + $sheet->getCell('A1')->setValue("=TIMEVALUE($timeValue)"); + $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } @@ -35,18 +29,18 @@ class TimeValueTest extends TestCase public function testTIMEVALUEtoUnixTimestamp(): void { - Functions::setReturnDateType(Functions::RETURNDATE_UNIX_TIMESTAMP); + self::setUnixReturn(); - $result = DateTime::TIMEVALUE('7:30:20'); + $result = TimeValue::funcTimeValue('7:30:20'); self::assertEquals(23420, $result); self::assertEqualsWithDelta(23420, $result, 1E-8); } public function testTIMEVALUEtoDateTimeObject(): void { - Functions::setReturnDateType(Functions::RETURNDATE_PHP_DATETIME_OBJECT); + self::setObjectReturn(); - $result = DateTime::TIMEVALUE('7:30:20'); + $result = TimeValue::funcTimeValue('7:30:20'); // Must return an object... self::assertIsObject($result); // ... of the correct type diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TodayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TodayTest.php new file mode 100644 index 00000000..6ce82bfd --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TodayTest.php @@ -0,0 +1,34 @@ +sheet; + // Loop to avoid rare edge case where first calculation + // and second do not take place in same second. + do { + $dtStart = new DateTimeImmutable(); + $startSecond = $dtStart->format('s'); + $sheet->setCellValue('A1', '=TODAY()'); + $dtEnd = new DateTimeImmutable(); + $endSecond = $dtEnd->format('s'); + } while ($startSecond !== $endSecond); + $sheet->setCellValue('B1', '=YEAR(A1)'); + $sheet->setCellValue('C1', '=MONTH(A1)'); + $sheet->setCellValue('D1', '=DAY(A1)'); + $sheet->setCellValue('E1', '=HOUR(A1)'); + $sheet->setCellValue('F1', '=MINUTE(A1)'); + $sheet->setCellValue('G1', '=SECOND(A1)'); + self::assertSame((int) $dtStart->format('Y'), $sheet->getCell('B1')->getCalculatedValue()); + self::assertSame((int) $dtStart->format('m'), $sheet->getCell('C1')->getCalculatedValue()); + self::assertSame((int) $dtStart->format('d'), $sheet->getCell('D1')->getCalculatedValue()); + self::assertSame(0, $sheet->getCell('E1')->getCalculatedValue()); + self::assertSame(0, $sheet->getCell('F1')->getCalculatedValue()); + self::assertSame(0, $sheet->getCell('G1')->getCalculatedValue()); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php index 99aa6f7c..f1bc51f3 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php @@ -2,33 +2,22 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Weekday; -class WeekDayTest extends TestCase +class WeekDayTest extends AllSetupTeardown { - private $excelCalendar; - - protected function setUp(): void - { - $this->excelCalendar = Date::getExcelCalendar(); - } - - protected function tearDown(): void - { - Date::setExcelCalendar($this->excelCalendar); - } - /** * @dataProvider providerWEEKDAY * * @param mixed $expectedResult */ - public function testWEEKDAY($expectedResult, ...$args): void + public function testWEEKDAY($expectedResult, string $formula): void { - $result = DateTime::WEEKDAY(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('1954-11-23'); + $sheet->getCell('A1')->setValue("=WEEKDAY($formula)"); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerWEEKDAY() @@ -38,9 +27,9 @@ class WeekDayTest extends TestCase public function testWEEKDAYwith1904Calendar(): void { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); - self::assertEquals(7, DateTime::WEEKDAY('1904-01-02')); - self::assertEquals(6, DateTime::WEEKDAY('1904-01-01')); - self::assertEquals(6, DateTime::WEEKDAY(null)); + self::setMac1904(); + self::assertEquals(7, Weekday::funcWeekDay('1904-01-02')); + self::assertEquals(6, Weekday::funcWeekDay('1904-01-01')); + self::assertEquals(6, Weekday::funcWeekDay(null)); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php index 17119f28..c3e785f3 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekNumTest.php @@ -2,33 +2,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class WeekNumTest extends TestCase +class WeekNumTest extends AllSetupTeardown { - private $excelCalendar; - - protected function setUp(): void - { - $this->excelCalendar = Date::getExcelCalendar(); - } - - protected function tearDown(): void - { - Date::setExcelCalendar($this->excelCalendar); - } - /** * @dataProvider providerWEEKNUM * * @param mixed $expectedResult */ - public function testWEEKNUM($expectedResult, ...$args): void + public function testWEEKNUM($expectedResult, string $formula): void { - $result = DateTime::WEEKNUM(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('1954-11-23'); + $sheet->getCell('A1')->setValue("=WEEKNUM($formula)"); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerWEEKNUM() @@ -36,13 +23,23 @@ class WeekNumTest extends TestCase return require 'tests/data/Calculation/DateTime/WEEKNUM.php'; } - public function testWEEKNUMwith1904Calendar(): void + /** + * @dataProvider providerWEEKNUM1904 + * + * @param mixed $expectedResult + */ + public function testWEEKNUM1904($expectedResult, string $formula): void { - Date::setExcelCalendar(Date::CALENDAR_MAC_1904); - self::assertEquals(27, DateTime::WEEKNUM('2004-07-02')); - self::assertEquals(1, DateTime::WEEKNUM('1904-01-02')); - self::assertEquals(1, DateTime::WEEKNUM(null)); - // The following is a bug in Excel. - self::assertEquals(0, DateTime::WEEKNUM('1904-01-01')); + $this->mightHaveException($expectedResult); + self::setMac1904(); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('1954-11-23'); + $sheet->getCell('A1')->setValue("=WEEKNUM($formula)"); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); + } + + public function providerWEEKNUM1904() + { + return require 'tests/data/Calculation/DateTime/WEEKNUM1904.php'; } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WorkDayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WorkDayTest.php index 4784e463..ec2a5402 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WorkDayTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WorkDayTest.php @@ -2,29 +2,47 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class WorkDayTest extends TestCase +class WorkDayTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerWORKDAY * * @param mixed $expectedResult + * @param mixed $arg1 + * @param mixed $arg2 */ - public function testWORKDAY($expectedResult, ...$args): void + public function testWORKDAY($expectedResult, $arg1 = 'omitted', $arg2 = 'omitted', ?array $arg3 = null): void { - $result = DateTime::WORKDAY(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('A1')->setValue($arg1); + } + if ($arg2 !== null) { + $sheet->getCell('A2')->setValue($arg2); + } + $dateArray = []; + if (is_array($arg3)) { + if (array_key_exists(0, $arg3) && is_array($arg3[0])) { + $dateArray = $arg3[0]; + } else { + $dateArray = $arg3; + } + } + $dateIndex = 0; + foreach ($dateArray as $date) { + ++$dateIndex; + $sheet->getCell("C$dateIndex")->setValue($date); + } + $arrayArg = $dateIndex ? ", C1:C$dateIndex" : ''; + if ($arg1 === 'omitted') { + $sheet->getCell('B1')->setValue('=WORKDAY()'); + } elseif ($arg2 === 'omitted') { + $sheet->getCell('B1')->setValue('=WORKDAY(A1)'); + } else { + $sheet->getCell('B1')->setValue("=WORKDAY(A1, A2$arrayArg)"); + } + self::assertEquals($expectedResult, $sheet->getCell('B1')->getCalculatedValue()); } public function providerWORKDAY() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearFracTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearFracTest.php index 05f11310..e6ac823a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearFracTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearFracTest.php @@ -2,29 +2,39 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class YearFracTest extends TestCase +class YearFracTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerYEARFRAC * * @param mixed $expectedResult + * @param mixed $arg1 + * @param mixed $arg2 + * @param mixed $arg3 */ - public function testYEARFRAC($expectedResult, ...$args): void + public function testYEARFRAC($expectedResult, $arg1 = 'omitted', $arg2 = 'omitted', $arg3 = 'omitted'): void { - $result = DateTime::YEARFRAC(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('A1')->setValue($arg1); + } + if ($arg2 !== null) { + $sheet->getCell('A2')->setValue($arg2); + } + if ($arg3 !== null) { + $sheet->getCell('A3')->setValue($arg3); + } + if ($arg1 === 'omitted') { + $sheet->getCell('B1')->setValue('=YEARFRAC()'); + } elseif ($arg2 === 'omitted') { + $sheet->getCell('B1')->setValue('=YEARFRAC(A1)'); + } elseif ($arg3 === 'omitted') { + $sheet->getCell('B1')->setValue('=YEARFRAC(A1, A2)'); + } else { + $sheet->getCell('B1')->setValue('=YEARFRAC(A1, A2, A3)'); + } + self::assertEqualswithDelta($expectedResult, $sheet->getCell('B1')->getCalculatedValue(), 1E-6); } public function providerYEARFRAC() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearTest.php index bbdaf92a..7942f06c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/YearTest.php @@ -2,30 +2,20 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PHPUnit\Framework\TestCase; - -class YearTest extends TestCase +class YearTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - Date::setExcelCalendar(Date::CALENDAR_WINDOWS_1900); - } - /** * @dataProvider providerYEAR * * @param mixed $expectedResult - * @param $dateTimeValue */ - public function testYEAR($expectedResult, $dateTimeValue): void + public function testYEAR($expectedResult, string $dateTimeValue): void { - $result = DateTime::YEAR($dateTimeValue); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue("=YEAR($dateTimeValue)"); + $sheet->getCell('B1')->setValue('1954-11-23'); + self::assertSame($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); } public function providerYEAR() diff --git a/tests/data/Calculation/DateTime/DATE.php b/tests/data/Calculation/DateTime/DATE.php index 9acc6716..72816b76 100644 --- a/tests/data/Calculation/DateTime/DATE.php +++ b/tests/data/Calculation/DateTime/DATE.php @@ -1,319 +1,84 @@ [ - 6890, // '11th November 1918' - 18, 11, 11, - ], - 'Excel 1900 Calendar Base Date' => [ - 1, - 1900, 1, 1, - ], - 'Day before Excel mythical 1900 leap day' => [ - 59, - 1900, 2, 28, - ], - 'Excel mythical 1900 leap day' => [ - 60, - 1900, 2, 29, - ], - 'Day after Excel mythical 1900 leap day' => [ - 61, - 1900, 3, 1, - ], - 'Day after Excel actual 1904 leap day' => [ - 713, - 1901, 12, 13, - ], - 'signed 32-bit Unix Timestamp Earliest Date' => [ - 714, - 1901, 12, 14, - ], - 'Day before Excel 1904 Calendar Base Date' => [ - 1461, - 1903, 12, 31, - ], - 'Excel 1904 Calendar Base Date' => [ - 1462, - 1904, 1, 1, - ], - 'Day after Excel 1904 Calendar Base Date' => [ - 1463, - 1904, 1, 2, - ], - [ - 22269, - 1960, 12, 19, - ], - 'Unix Timestamp Base Date' => [ - 25569, - 1970, 1, 1, - ], - [ - 30292, - 1982, 12, 7, - ], - [ - 39611, - 2008, 6, 12, - ], - '32-bit signed Unix Timestamp Latest Date' => [ - 50424, - 2038, 1, 19, - ], - 'Day after 32-bit signed Unix Timestamp Latest Date' => [ - 50425, - 2038, 1, 20, - ], - [ - 39448, - 2008, 1, 1, - ], - [ - 39447, - 2008, 1, null, - ], - [ - 39446, - 2008, 1, -1, - ], - [ - 39417, - 2008, 1, -30, - ], - [ - 39416, - 2008, 1, -31, - ], - [ - 39082, - 2008, 1, -365, - ], - [ - 39508, - 2008, 3, 1, - ], - [ - 39507, - 2008, 3, null, - ], - [ - 39506, - 2008, 3, -1, - ], - [ - 39142, - 2008, 3, -365, - ], - [ - 39417, - 2008, null, 1, - ], - [ - 39387, - 2008, -1, 1, - ], - [ - 39083, - 2008, -11, 1, - ], - [ - 39052, - 2008, -12, 1, - ], - [ - 39022, - 2008, -13, 1, - ], - [ - 39051, - 2008, -13, 30, - ], - [ - 39021, - 2008, -13, null, - ], - [ - 38991, - 2008, -13, -30, - ], - [ - 38990, - 2008, -13, -31, - ], - [ - 39814, - 2008, 13, 1, - ], - [ - 39507, - 2007, 15, null, - ], - [ - 40210, - 2008, 26, 1, - ], - [ - 40199, - 2008, 26, -10, - ], - [ - 38686, - 2008, -26, 61, - ], - [ - 39641, - 2010, -15, -50, - ], - [ - 39741, - 2010, -15, 50, - ], - [ - 40552, - 2010, 15, -50, - ], - [ - 40652, - 2010, 15, 50, - ], - [ - 40179, - 2010, 1.5, 1, - ], - [ - 40178, - 2010, 1.5, 0, - ], - [ - 40148, - 2010, 0, 1.5, - ], - [ - 40179, - 2010, 1, 1.5, - ], - [ - 41075, - 2012, 6, 15, - ], - [ - 41060, - 2012, 6, null, - ], - [ - 40892, - 2012, null, 15, - ], - [ - 167, - null, 6, 15, - ], - [ - 3819, - 10, 6, 15, - ], - [ - 3622, - 10, null, null, - ], - [ - 274, - null, 10, null, - ], - [ - '#NUM!', - null, null, 10, - ], - [ - '#NUM!', - -20, null, null, - ], - [ - '#NUM!', - -20, 6, 15, - ], - 'Excel Maximum Date' => [ - 2958465, - 9999, 12, 31, - ], - 'Exceeded Excel Maximum Date' => [ - '#NUM!', - 10000, 1, 1, - ], - [ - 39670, - 2008, 8, 10, - ], - [ - 39813, - 2008, 12, 31, - ], - [ - 39692, - 2008, 8, 32, - ], - [ - 39844, - 2008, 13, 31, - ], - [ - 39813, - 2009, 1, 0, - ], - [ - 39812, - 2009, 1, -1, - ], - [ - 39782, - 2009, 0, 0, - ], - [ - 39781, - 2009, 0, -1, - ], - [ - 39752, - 2009, -1, 0, - ], - [ - 39751, - 2009, -1, -1, - ], - [ - 40146, - 2010, 0, -1, - ], - [ - 40329, - 2010, 5, 31, - ], + [6890, '18, 11, 11'], // year without centure + [1, '1900, 1, 1'], // Excel 1900 Calendar BaseDate + [59, '1900, 2, 28'], // Day before Excel mythical 1900 leap day + [60, '1900, 2, 29'], // Excel mythical 1900 leap day + [61, '1900, 3, 1'], // Day after Excel mythical 1900 leap day + [713, '1901, 12, 13'], // Day after actual 1904 leap day + [714, '1901, 12, 14'], // signed 32-bit Unix Timestamp Earliest Date + [1461, '1903, 12, 31'], // Day before Excel 1904 Calendar Base Date + [1462, '1904, 1, 1'], // Excel 1904 Calendar Base Date + [1463, '1904, 1, 2'], // Day after Excel 1904 Calendar Base Date + [22269, '1960, 12, 19'], + [25569, '1970, 1, 1'], // Unix Timestamp Base Date + [30292, '1982, 12, 7'], + [39611, '2008, 6, 12'], + [50424, '2038, 1, 19'], // 32-bit signed Unix Timestamp Latest Date + [50425, '2038, 1, 20'], // Day after 32-bit signed Unix Timestamp Latest Date + [39448, '2008, 1, 1'], + [39447, '2008, 1, Q15'], + [39446, '2008, 1, -1'], + [39417, '2008, 1, -30'], + [39416, '2008, 1, -31'], + [39082, '2008, 1, -365'], + [39508, '2008, 3, 1'], + [39507, '2008, 3, Q15'], + [39506, '2008, 3, -1'], + [39142, '2008, 3, -365'], + [39417, '2008, Q15, 1'], + [39387, '2008, -1, 1'], + [39083, '2008, -11, 1'], + [39052, '2008, -12, 1'], + [39022, '2008, -13, 1'], + [39051, '2008, -13, 30'], + [39021, '2008, -13, Q15'], + [38991, '2008, -13, -30'], + [38990, '2008, -13, -31'], + [39814, '2008, 13, 1'], + [39507, '2007, 15, Q15'], + [40210, '2008, 26, 1'], + [40199, '2008, 26, -10'], + [38686, '2008, -26, 61'], + [39641, '2010, -15, -50'], + [39741, '2010, -15, 50'], + [40552, '2010, 15, -50'], + [40652, '2010, 15, 50'], + [40179, '2010, 1.5, 1'], + [40178, '2010, 1.5, 0'], + [40148, '2010, 0, 1.5'], + [40179, '2010, 1, 1.5'], + [41075, '2012, 6, 15'], + [41060, '2012, 6, Q15'], + [40892, '2012, Q15, 15'], + [167, 'Q15, 6, 15'], + [3819, '10, 6, 15'], + [3622, '10, Q15, Q16'], + [274, 'Q14, 10, Q15'], + ['#NUM!', 'Q14, Q15, 10'], + ['#NUM!', '-20, Q14, Q15'], + ['#NUM!', '-20, 6, 15'], + [2958465, '9999, 12, 31'], // Excel maximum date + ['#NUM!', '10000, 1, 1'], // Exceeded Excel maximum date + [39670, '2008, 8, 10'], + [39813, '2008, 12, 31'], + [39692, '2008, 8, 32'], + [39844, '2008, 13, 31'], + [39813, '2009, 1, 0'], + [39812, '2009, 1, -1'], + [39782, '2009, 0, 0'], + [39781, '2009, 0, -1'], + [39752, '2009, -1, 0'], + [39751, '2009, -1, -1'], + [40146, '2010, 0, -1'], + [40329, '2010, 5, 31'], + [40199, '2010, 1, "21st"'], // Excel can't parse ordinal, PhpSpreadsheet can + [40258, '2010, "March", "21st"'], // ordinal and month name // MS Excel will fail with a #VALUE return, but PhpSpreadsheet can parse this date - [ - 40199, - 2010, 1, '21st', - ], - // MS Excel will fail with a #VALUE return, but PhpSpreadsheet can parse this date - [ - 40258, - 2010, 'March', '21st', - ], - // MS Excel will fail with a #VALUE return, but PhpSpreadsheet can parse this date - [ - 40258, - 2010, 'March', 21, - ], - [ - '#VALUE!', - 'ABC', 1, 21, - ], - [ - '#VALUE!', - 2010, 'DEF', 21, - ], - [ - '#VALUE!', - 2010, 3, 'GHI', - ], + [40258, '2010, "March", 21'], // Excel can't parse month name, PhpSpreadsheet can + ['#VALUE!', '"ABC", 1, 21'], + ['#VALUE!', '2010, "DEF", 21'], + ['#VALUE!', '2010, 3, "GHI"'], + ['exception', '2010, 3'], ]; diff --git a/tests/data/Calculation/DateTime/DATEDIF.php b/tests/data/Calculation/DateTime/DATEDIF.php index a6d2d761..5ba0bd3c 100644 --- a/tests/data/Calculation/DateTime/DATEDIF.php +++ b/tests/data/Calculation/DateTime/DATEDIF.php @@ -1,424 +1,112 @@ Date: Sun, 21 Mar 2021 21:40:49 +0100 Subject: [PATCH 57/89] Financial functions next stage of refactoring (#1943) * First steps splitting out the Amortization and Deprecation Excel functions from Financials * Verify which methods allow negative values for arguments * Additional unit tests for SLN() and SYD() * Additional unit tests for DDB() * Additional unit tests for DB() * Verify Amortization cases where salvage is greater than cost * More unit tests for Amortization * Resolve broken test in AMORLINC() and extract amortizationCoefficient calculation * verify amortizationCoefficient calculation * Extract YIELDDISC() and YIELDMAT() to Financial\Securities * Additional validation for Securities Yield functions --- .../Calculation/Calculation.php | 18 +- src/PhpSpreadsheet/Calculation/Financial.php | 337 ++++-------------- .../Calculation/Financial/Amortization.php | 163 +++++++++ .../Calculation/Financial/Depreciation.php | 287 +++++++++++++++ .../Financial/Securities/BaseValidations.php | 145 ++++++++ .../Financial/Securities/Constants.php | 10 + .../{Securities.php => Securities/Price.php} | 134 +------ .../Financial/Securities/Yields.php | 136 +++++++ .../data/Calculation/Financial/AMORDEGRC.php | 28 ++ tests/data/Calculation/Financial/AMORLINC.php | 16 +- tests/data/Calculation/Financial/DB.php | 121 ++++++- tests/data/Calculation/Financial/DDB.php | 113 ++++++ tests/data/Calculation/Financial/SLN.php | 34 +- tests/data/Calculation/Financial/SYD.php | 52 +++ .../data/Calculation/Financial/YIELDDISC.php | 24 ++ tests/data/Calculation/Financial/YIELDMAT.php | 28 ++ 16 files changed, 1225 insertions(+), 421 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Amortization.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Depreciation.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Securities/Constants.php rename src/PhpSpreadsheet/Calculation/Financial/{Securities.php => Securities/Price.php} (73%) create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index dbea0850..9317699b 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -273,12 +273,12 @@ class Calculation ], 'AMORDEGRC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'AMORDEGRC'], + 'functionCall' => [Financial\Amortization::class, 'AMORDEGRC'], 'argumentCount' => '6,7', ], 'AMORLINC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'AMORLINC'], + 'functionCall' => [Financial\Amortization::class, 'AMORLINC'], 'argumentCount' => '6,7', ], 'AND' => [ @@ -1983,17 +1983,17 @@ class Calculation ], 'PRICE' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial\Securities::class, 'price'], + 'functionCall' => [Financial\Securities\Price::class, 'price'], 'argumentCount' => '6,7', ], 'PRICEDISC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial\Securities::class, 'discounted'], + 'functionCall' => [Financial\Securities\Price::class, 'priceDiscounted'], 'argumentCount' => '4,5', ], 'PRICEMAT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial\Securities::class, 'maturity'], + 'functionCall' => [Financial\Securities\Price::class, 'priceAtMaturity'], 'argumentCount' => '5,6', ], 'PROB' => [ @@ -2225,7 +2225,7 @@ class Calculation ], 'SLN' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'SLN'], + 'functionCall' => [Financial\Depreciation::class, 'SLN'], 'argumentCount' => '3', ], 'SLOPE' => [ @@ -2356,7 +2356,7 @@ class Calculation ], 'SYD' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'SYD'], + 'functionCall' => [Financial\Depreciation::class, 'SYD'], 'argumentCount' => '4', ], 'T' => [ @@ -2641,12 +2641,12 @@ class Calculation ], 'YIELDDISC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'YIELDDISC'], + 'functionCall' => [Financial\Securities\Yields::class, 'yieldDiscounted'], 'argumentCount' => '4,5', ], 'YIELDMAT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'YIELDMAT'], + 'functionCall' => [Financial\Securities\Yields::class, 'yieldAtMaturity'], 'argumentCount' => '5,6', ], 'ZTEST' => [ diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 9735e9f4..2b54e1cd 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -2,8 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\Amortization; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\Depreciation; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\Dollar; use PhpOffice\PhpSpreadsheet\Calculation\Financial\InterestRate; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\TreasuryBill; class Financial { @@ -147,6 +152,10 @@ class Financial * Excel Function: * AMORDEGRC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the AMORDEGRC() method in the Financial\Amortization class instead + * * @param float $cost The cost of the asset * @param mixed $purchased Date of the purchase of the asset * @param mixed $firstPeriod Date of the end of the first period @@ -164,57 +173,7 @@ class Financial */ public static function AMORDEGRC($cost, $purchased, $firstPeriod, $salvage, $period, $rate, $basis = 0) { - $cost = Functions::flattenSingleValue($cost); - $purchased = Functions::flattenSingleValue($purchased); - $firstPeriod = Functions::flattenSingleValue($firstPeriod); - $salvage = Functions::flattenSingleValue($salvage); - $period = floor(Functions::flattenSingleValue($period)); - $rate = Functions::flattenSingleValue($rate); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - $yearFrac = DateTime::YEARFRAC($purchased, $firstPeriod, $basis); - if (is_string($yearFrac)) { - return $yearFrac; - } - - // The depreciation coefficients are: - // Life of assets (1/rate) Depreciation coefficient - // Less than 3 years 1 - // Between 3 and 4 years 1.5 - // Between 5 and 6 years 2 - // More than 6 years 2.5 - $fUsePer = 1.0 / $rate; - if ($fUsePer < 3.0) { - $amortiseCoeff = 1.0; - } elseif ($fUsePer < 5.0) { - $amortiseCoeff = 1.5; - } elseif ($fUsePer <= 6.0) { - $amortiseCoeff = 2.0; - } else { - $amortiseCoeff = 2.5; - } - - $rate *= $amortiseCoeff; - $fNRate = round($yearFrac * $rate * $cost, 0); - $cost -= $fNRate; - $fRest = $cost - $salvage; - - for ($n = 0; $n < $period; ++$n) { - $fNRate = round($rate * $cost, 0); - $fRest -= $fNRate; - - if ($fRest < 0.0) { - switch ($period - $n) { - case 0: - case 1: - return round($cost * 0.5, 0); - default: - return 0.0; - } - } - $cost -= $fNRate; - } - - return $fNRate; + return Amortization::AMORDEGRC($cost, $purchased, $firstPeriod, $salvage, $period, $rate, $basis); } /** @@ -227,6 +186,10 @@ class Financial * Excel Function: * AMORLINC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) * + * @Deprecated 1.18.0 + * + * @see Use the AMORLINC() method in the Financial\Amortization class instead + * * @param float $cost The cost of the asset * @param mixed $purchased Date of the purchase of the asset * @param mixed $firstPeriod Date of the end of the first period @@ -244,39 +207,7 @@ class Financial */ public static function AMORLINC($cost, $purchased, $firstPeriod, $salvage, $period, $rate, $basis = 0) { - $cost = Functions::flattenSingleValue($cost); - $purchased = Functions::flattenSingleValue($purchased); - $firstPeriod = Functions::flattenSingleValue($firstPeriod); - $salvage = Functions::flattenSingleValue($salvage); - $period = Functions::flattenSingleValue($period); - $rate = Functions::flattenSingleValue($rate); - $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - - $fOneRate = $cost * $rate; - $fCostDelta = $cost - $salvage; - // Note, quirky variation for leap years on the YEARFRAC for this function - $purchasedYear = DateTime::YEAR($purchased); - $yearFrac = DateTime::YEARFRAC($purchased, $firstPeriod, $basis); - if (is_string($yearFrac)) { - return $yearFrac; - } - - if (($basis == 1) && ($yearFrac < 1) && (DateTime::isLeapYear($purchasedYear))) { - $yearFrac *= 365 / 366; - } - - $f0Rate = $yearFrac * $rate * $cost; - $nNumOfFullPeriods = (int) (($cost - $salvage - $f0Rate) / $fOneRate); - - if ($period == 0) { - return $f0Rate; - } elseif ($period <= $nNumOfFullPeriods) { - return $fOneRate; - } elseif ($period == ($nNumOfFullPeriods + 1)) { - return $fCostDelta - $fOneRate * $nNumOfFullPeriods - $f0Rate; - } - - return 0.0; + return Amortization::AMORLINC($cost, $purchased, $firstPeriod, $salvage, $period, $rate, $basis); } /** @@ -613,6 +544,10 @@ class Financial * Excel Function: * DB(cost,salvage,life,period[,month]) * + * @Deprecated 1.18.0 + * + * @see Use the DB() method in the Financial\Depreciation class instead + * * @param float $cost Initial cost of the asset * @param float $salvage Value at the end of the depreciation. * (Sometimes called the salvage value of the asset) @@ -627,46 +562,7 @@ class Financial */ public static function DB($cost, $salvage, $life, $period, $month = 12) { - $cost = Functions::flattenSingleValue($cost); - $salvage = Functions::flattenSingleValue($salvage); - $life = Functions::flattenSingleValue($life); - $period = Functions::flattenSingleValue($period); - $month = Functions::flattenSingleValue($month); - - // Validate - if ((is_numeric($cost)) && (is_numeric($salvage)) && (is_numeric($life)) && (is_numeric($period)) && (is_numeric($month))) { - $cost = (float) $cost; - $salvage = (float) $salvage; - $life = (int) $life; - $period = (int) $period; - $month = (int) $month; - if ($cost == 0) { - return 0.0; - } elseif (($cost < 0) || (($salvage / $cost) < 0) || ($life <= 0) || ($period < 1) || ($month < 1)) { - return Functions::NAN(); - } - // Set Fixed Depreciation Rate - $fixedDepreciationRate = 1 - ($salvage / $cost) ** (1 / $life); - $fixedDepreciationRate = round($fixedDepreciationRate, 3); - - // Loop through each period calculating the depreciation - $previousDepreciation = 0; - $depreciation = 0; - for ($per = 1; $per <= $period; ++$per) { - if ($per == 1) { - $depreciation = $cost * $fixedDepreciationRate * $month / 12; - } elseif ($per == ($life + 1)) { - $depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate * (12 - $month) / 12; - } else { - $depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate; - } - $previousDepreciation += $depreciation; - } - - return $depreciation; - } - - return Functions::VALUE(); + return Depreciation::DB($cost, $salvage, $life, $period, $month); } /** @@ -678,6 +574,10 @@ class Financial * Excel Function: * DDB(cost,salvage,life,period[,factor]) * + * @Deprecated 1.18.0 + * + * @see Use the DDB() method in the Financial\Depreciation class instead + * * @param float $cost Initial cost of the asset * @param float $salvage Value at the end of the depreciation. * (Sometimes called the salvage value of the asset) @@ -693,38 +593,7 @@ class Financial */ public static function DDB($cost, $salvage, $life, $period, $factor = 2.0) { - $cost = Functions::flattenSingleValue($cost); - $salvage = Functions::flattenSingleValue($salvage); - $life = Functions::flattenSingleValue($life); - $period = Functions::flattenSingleValue($period); - $factor = Functions::flattenSingleValue($factor); - - // Validate - if ((is_numeric($cost)) && (is_numeric($salvage)) && (is_numeric($life)) && (is_numeric($period)) && (is_numeric($factor))) { - $cost = (float) $cost; - $salvage = (float) $salvage; - $life = (int) $life; - $period = (int) $period; - $factor = (float) $factor; - if (($cost <= 0) || (($salvage / $cost) < 0) || ($life <= 0) || ($period < 1) || ($factor <= 0.0) || ($period > $life)) { - return Functions::NAN(); - } - // Set Fixed Depreciation Rate - $fixedDepreciationRate = 1 - ($salvage / $cost) ** (1 / $life); - $fixedDepreciationRate = round($fixedDepreciationRate, 3); - - // Loop through each period calculating the depreciation - $previousDepreciation = 0; - $depreciation = 0; - for ($per = 1; $per <= $period; ++$per) { - $depreciation = min(($cost - $previousDepreciation) * ($factor / $life), ($cost - $salvage - $previousDepreciation)); - $previousDepreciation += $depreciation; - } - - return $depreciation; - } - - return Functions::VALUE(); + return Depreciation::DDB($cost, $salvage, $life, $period, $factor); } /** @@ -800,7 +669,7 @@ class Financial */ public static function DOLLARDE($fractional_dollar = null, $fraction = 0) { - return Financial\Dollar::decimal($fractional_dollar, $fraction); + return Dollar::decimal($fractional_dollar, $fraction); } /** @@ -824,7 +693,7 @@ class Financial */ public static function DOLLARFR($decimal_dollar = null, $fraction = 0) { - return Financial\Dollar::fractional($decimal_dollar, $fraction); + return Dollar::fractional($decimal_dollar, $fraction); } /** @@ -1368,7 +1237,7 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the price() method in the Financial\Securities class instead + * @see Use the price() method in the Financial\Securities\Price class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue date when the security @@ -1393,7 +1262,7 @@ class Financial */ public static function PRICE($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis = 0) { - return Financial\Securities::price($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis); + return Securities\Price::price($settlement, $maturity, $rate, $yield, $redemption, $frequency, $basis); } /** @@ -1403,12 +1272,13 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the discounted() method in the Financial\Securities class instead + * @see Use the priceDiscounted() method in the Financial\Securities\Price class instead * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue date when the security is traded to the buyer. + * The security settlement date is the date after the issue date when the security + * is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. + * The maturity date is the date when the security expires. * @param int $discount The security's discount rate * @param int $redemption The security's redemption value per $100 face value * @param int $basis The type of day count to use. @@ -1422,7 +1292,7 @@ class Financial */ public static function PRICEDISC($settlement, $maturity, $discount, $redemption, $basis = 0) { - return Financial\Securities::discounted($settlement, $maturity, $discount, $redemption, $basis); + return Securities\Price::priceDiscounted($settlement, $maturity, $discount, $redemption, $basis); } /** @@ -1432,12 +1302,13 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the maturity() method in the Financial\Securities class instead + * @see Use the priceAtMaturity() method in the Financial\Securities\Price class instead * * @param mixed $settlement The security's settlement date. - * The security's settlement date is the date after the issue date when the security is traded to the buyer. + * The security's settlement date is the date after the issue date when the security + * is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. + * The maturity date is the date when the security expires. * @param mixed $issue The security's issue date * @param int $rate The security's interest rate at date of issue * @param int $yield The security's annual yield @@ -1452,7 +1323,7 @@ class Financial */ public static function PRICEMAT($settlement, $maturity, $issue, $rate, $yield, $basis = 0) { - return Financial\Securities::maturity($settlement, $maturity, $issue, $rate, $yield, $basis); + return Securities\Price::priceAtMaturity($settlement, $maturity, $issue, $rate, $yield, $basis); } /** @@ -1640,6 +1511,10 @@ class Financial * * Returns the straight-line depreciation of an asset for one period * + * @Deprecated 1.18.0 + * + * @see Use the SLN() method in the Financial\Depreciation class instead + * * @param mixed $cost Initial cost of the asset * @param mixed $salvage Value at the end of the depreciation * @param mixed $life Number of periods over which the asset is depreciated @@ -1648,20 +1523,7 @@ class Financial */ public static function SLN($cost, $salvage, $life) { - $cost = Functions::flattenSingleValue($cost); - $salvage = Functions::flattenSingleValue($salvage); - $life = Functions::flattenSingleValue($life); - - // Calculate - if ((is_numeric($cost)) && (is_numeric($salvage)) && (is_numeric($life))) { - if ($life < 0) { - return Functions::NAN(); - } - - return ($cost - $salvage) / $life; - } - - return Functions::VALUE(); + return Depreciation::SLN($cost, $salvage, $life); } /** @@ -1669,6 +1531,10 @@ class Financial * * Returns the sum-of-years' digits depreciation of an asset for a specified period. * + * @Deprecated 1.18.0 + * + * @see Use the SYD() method in the Financial\Depreciation class instead + * * @param mixed $cost Initial cost of the asset * @param mixed $salvage Value at the end of the depreciation * @param mixed $life Number of periods over which the asset is depreciated @@ -1678,21 +1544,7 @@ class Financial */ public static function SYD($cost, $salvage, $life, $period) { - $cost = Functions::flattenSingleValue($cost); - $salvage = Functions::flattenSingleValue($salvage); - $life = Functions::flattenSingleValue($life); - $period = Functions::flattenSingleValue($period); - - // Calculate - if ((is_numeric($cost)) && (is_numeric($salvage)) && (is_numeric($life)) && (is_numeric($period))) { - if (($life < 1) || ($period > $life)) { - return Functions::NAN(); - } - - return (($cost - $salvage) * ($life - $period + 1) * 2) / ($life * ($life + 1)); - } - - return Functions::VALUE(); + return Depreciation::SYD($cost, $salvage, $life, $period); } /** @@ -1714,7 +1566,7 @@ class Financial */ public static function TBILLEQ($settlement, $maturity, $discount) { - return Financial\TreasuryBill::bondEquivalentYield($settlement, $maturity, $discount); + return TreasuryBill::bondEquivalentYield($settlement, $maturity, $discount); } /** @@ -1737,7 +1589,7 @@ class Financial */ public static function TBILLPRICE($settlement, $maturity, $discount) { - return Financial\TreasuryBill::price($settlement, $maturity, $discount); + return TreasuryBill::price($settlement, $maturity, $discount); } /** @@ -1760,7 +1612,7 @@ class Financial */ public static function TBILLYIELD($settlement, $maturity, $price) { - return Financial\TreasuryBill::yield($settlement, $maturity, $price); + return TreasuryBill::yield($settlement, $maturity, $price); } private static function bothNegAndPos($neg, $pos) @@ -1977,10 +1829,13 @@ class Financial * * Returns the annual yield of a security that pays interest at maturity. * + * @see Use the yieldDiscounted() method in the Financial\Securities\Yields class instead + * * @param mixed $settlement The security's settlement date. - * The security's settlement date is the date after the issue date when the security is traded to the buyer. + * The security's settlement date is the date after the issue date when the security + * is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. + * The maturity date is the date when the security expires. * @param int $price The security's price per $100 face value * @param int $redemption The security's redemption value per $100 face value * @param int $basis The type of day count to use. @@ -1994,32 +1849,7 @@ class Financial */ public static function YIELDDISC($settlement, $maturity, $price, $redemption, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $price = Functions::flattenSingleValue($price); - $redemption = Functions::flattenSingleValue($redemption); - $basis = (int) Functions::flattenSingleValue($basis); - - // Validate - if (is_numeric($price) && is_numeric($redemption)) { - if (($price <= 0) || ($redemption <= 0)) { - return Functions::NAN(); - } - $daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); - if (!is_numeric($daysPerYear)) { - return $daysPerYear; - } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - $daysBetweenSettlementAndMaturity *= $daysPerYear; - - return (($redemption - $price) / $price) * ($daysPerYear / $daysBetweenSettlementAndMaturity); - } - - return Functions::VALUE(); + return Securities\Yields::yieldDiscounted($settlement, $maturity, $price, $redemption, $basis); } /** @@ -2027,10 +1857,15 @@ class Financial * * Returns the annual yield of a security that pays interest at maturity. * + * @Deprecated 1.18.0 + * + * @see Use the yieldAtMaturity() method in the Financial\Securities\Yields class instead + * * @param mixed $settlement The security's settlement date. - * The security's settlement date is the date after the issue date when the security is traded to the buyer. + * The security's settlement date is the date after the issue date when the security + * is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. + * The maturity date is the date when the security expires. * @param mixed $issue The security's issue date * @param int $rate The security's interest rate at date of issue * @param int $price The security's price per $100 face value @@ -2045,46 +1880,6 @@ class Financial */ public static function YIELDMAT($settlement, $maturity, $issue, $rate, $price, $basis = 0) { - $settlement = Functions::flattenSingleValue($settlement); - $maturity = Functions::flattenSingleValue($maturity); - $issue = Functions::flattenSingleValue($issue); - $rate = Functions::flattenSingleValue($rate); - $price = Functions::flattenSingleValue($price); - $basis = (int) Functions::flattenSingleValue($basis); - - // Validate - if (is_numeric($rate) && is_numeric($price)) { - if (($rate <= 0) || ($price <= 0)) { - return Functions::NAN(); - } - $daysPerYear = Financial\Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); - if (!is_numeric($daysPerYear)) { - return $daysPerYear; - } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); - if (!is_numeric($daysBetweenIssueAndSettlement)) { - // return date error - return $daysBetweenIssueAndSettlement; - } - $daysBetweenIssueAndSettlement *= $daysPerYear; - $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); - if (!is_numeric($daysBetweenIssueAndMaturity)) { - // return date error - return $daysBetweenIssueAndMaturity; - } - $daysBetweenIssueAndMaturity *= $daysPerYear; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); - if (!is_numeric($daysBetweenSettlementAndMaturity)) { - // return date error - return $daysBetweenSettlementAndMaturity; - } - $daysBetweenSettlementAndMaturity *= $daysPerYear; - - return ((1 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate) - (($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) / - (($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) * - ($daysPerYear / $daysBetweenSettlementAndMaturity); - } - - return Functions::VALUE(); + return Securities\Yields::yieldAtMaturity($settlement, $maturity, $issue, $rate, $price, $basis); } } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php new file mode 100644 index 00000000..76be7e12 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php @@ -0,0 +1,163 @@ +getMessage(); + } + + if ($cost === 0.0) { + return 0.0; + } + + // Set Fixed Depreciation Rate + $fixedDepreciationRate = 1 - ($salvage / $cost) ** (1 / $life); + $fixedDepreciationRate = round($fixedDepreciationRate, 3); + + // Loop through each period calculating the depreciation + // TODO Handle period value between 0 and 1 (e.g. 0.5) + $previousDepreciation = 0; + $depreciation = 0; + for ($per = 1; $per <= $period; ++$per) { + if ($per == 1) { + $depreciation = $cost * $fixedDepreciationRate * $month / 12; + } elseif ($per == ($life + 1)) { + $depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate * (12 - $month) / 12; + } else { + $depreciation = ($cost - $previousDepreciation) * $fixedDepreciationRate; + } + $previousDepreciation += $depreciation; + } + + return $depreciation; + } + + /** + * DDB. + * + * Returns the depreciation of an asset for a specified period using the + * double-declining balance method or some other method you specify. + * + * Excel Function: + * DDB(cost,salvage,life,period[,factor]) + * + * @param float $cost Initial cost of the asset + * @param float $salvage Value at the end of the depreciation. + * (Sometimes called the salvage value of the asset) + * @param int $life Number of periods over which the asset is depreciated. + * (Sometimes called the useful life of the asset) + * @param int $period The period for which you want to calculate the + * depreciation. Period must use the same units as life. + * @param float $factor The rate at which the balance declines. + * If factor is omitted, it is assumed to be 2 (the + * double-declining balance method). + * + * @return float|string + */ + public static function DDB($cost, $salvage, $life, $period, $factor = 2.0) + { + $cost = Functions::flattenSingleValue($cost); + $salvage = Functions::flattenSingleValue($salvage); + $life = Functions::flattenSingleValue($life); + $period = Functions::flattenSingleValue($period); + $factor = Functions::flattenSingleValue($factor); + + try { + $cost = self::validateCost($cost); + $salvage = self::validateSalvage($salvage); + $life = self::validateLife($life); + $period = self::validatePeriod($period); + $factor = self::validateFactor($factor); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($period > $life) { + return Functions::NAN(); + } + + // Loop through each period calculating the depreciation + // TODO Handling for fractional $period values + $previousDepreciation = 0; + $depreciation = 0; + for ($per = 1; $per <= $period; ++$per) { + $depreciation = min(($cost - $previousDepreciation) * ($factor / $life), ($cost - $salvage - $previousDepreciation)); + $previousDepreciation += $depreciation; + } + + return $depreciation; + } + + /** + * SLN. + * + * Returns the straight-line depreciation of an asset for one period + * + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation + * @param mixed $life Number of periods over which the asset is depreciated + * + * @return float|string Result, or a string containing an error + */ + public static function SLN($cost, $salvage, $life) + { + $cost = Functions::flattenSingleValue($cost); + $salvage = Functions::flattenSingleValue($salvage); + $life = Functions::flattenSingleValue($life); + + try { + $cost = self::validateCost($cost, true); + $salvage = self::validateSalvage($salvage, true); + $life = self::validateLife($life, true); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($life === 0.0) { + return Functions::DIV0(); + } + + return ($cost - $salvage) / $life; + } + + /** + * SYD. + * + * Returns the sum-of-years' digits depreciation of an asset for a specified period. + * + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation + * @param mixed $life Number of periods over which the asset is depreciated + * @param mixed $period Period + * + * @return float|string Result, or a string containing an error + */ + public static function SYD($cost, $salvage, $life, $period) + { + $cost = Functions::flattenSingleValue($cost); + $salvage = Functions::flattenSingleValue($salvage); + $life = Functions::flattenSingleValue($life); + $period = Functions::flattenSingleValue($period); + + try { + $cost = self::validateCost($cost, true); + $salvage = self::validateSalvage($salvage); + $life = self::validateLife($life); + $period = self::validatePeriod($period); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($period > $life) { + return Functions::NAN(); + } + + $syd = (($cost - $salvage) * ($life - $period + 1) * 2) / ($life * ($life + 1)); + + return $syd; + } + + private static function validateCost($cost, bool $negativeValueAllowed = false): float + { + if (!is_numeric($cost)) { + throw new Exception(Functions::VALUE()); + } + + $cost = (float) $cost; + if ($cost < 0.0 && $negativeValueAllowed === false) { + throw new Exception(Functions::NAN()); + } + + return $cost; + } + + private static function validateSalvage($salvage, bool $negativeValueAllowed = false): float + { + if (!is_numeric($salvage)) { + throw new Exception(Functions::VALUE()); + } + + $salvage = (float) $salvage; + if ($salvage < 0.0 && $negativeValueAllowed === false) { + throw new Exception(Functions::NAN()); + } + + return $salvage; + } + + private static function validateLife($life, bool $negativeValueAllowed = false): float + { + if (!is_numeric($life)) { + throw new Exception(Functions::VALUE()); + } + + $life = (float) $life; + if ($life < 0.0 && $negativeValueAllowed === false) { + throw new Exception(Functions::NAN()); + } + + return $life; + } + + private static function validatePeriod($period, bool $negativeValueAllowed = false): float + { + if (!is_numeric($period)) { + throw new Exception(Functions::VALUE()); + } + + $period = (float) $period; + if ($period <= 0.0 && $negativeValueAllowed === false) { + throw new Exception(Functions::NAN()); + } + + return $period; + } + + private static function validateMonth($month): int + { + if (!is_numeric($month)) { + throw new Exception(Functions::VALUE()); + } + + $month = (int) $month; + if ($month < 1) { + throw new Exception(Functions::NAN()); + } + + return $month; + } + + private static function validateFactor($factor): float + { + if (!is_numeric($factor)) { + throw new Exception(Functions::VALUE()); + } + + $factor = (float) $factor; + if ($factor <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $factor; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php new file mode 100644 index 00000000..88cb8660 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php @@ -0,0 +1,145 @@ += $maturity) { + throw new Exception(Functions::NAN()); + } + } + + protected static function validateRate($rate): float + { + if (!is_numeric($rate)) { + throw new Exception(Functions::VALUE()); + } + + $rate = (float) $rate; + if ($rate < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $rate; + } + + protected static function validatePrice($price): float + { + if (!is_numeric($price)) { + throw new Exception(Functions::VALUE()); + } + + $price = (float) $price; + if ($price < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $price; + } + + protected static function validateYield($yield): float + { + if (!is_numeric($yield)) { + throw new Exception(Functions::VALUE()); + } + + $yield = (float) $yield; + if ($yield < 0.0) { + throw new Exception(Functions::NAN()); + } + + return $yield; + } + + protected static function validateRedemption($redemption): float + { + if (!is_numeric($redemption)) { + throw new Exception(Functions::VALUE()); + } + + $redemption = (float) $redemption; + if ($redemption <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $redemption; + } + + protected static function validateDiscount($discount): float + { + if (!is_numeric($discount)) { + throw new Exception(Functions::VALUE()); + } + + $discount = (float) $discount; + if ($discount <= 0.0) { + throw new Exception(Functions::NAN()); + } + + return $discount; + } + + protected static function validateFrequency($frequency): int + { + if (!is_numeric($frequency)) { + throw new Exception(Functions::VALUE()); + } + + $frequency = (int) $frequency; + if ( + ($frequency !== SecuritiesConstants::FREQUENCY_ANNUAL) && + ($frequency !== SecuritiesConstants::FREQUENCY_SEMI_ANNUAL) && + ($frequency !== SecuritiesConstants::FREQUENCY_QUARTERLY) + ) { + throw new Exception(Functions::NAN()); + } + + return $frequency; + } + + protected static function validateBasis($basis): int + { + if (!is_numeric($basis)) { + throw new Exception(Functions::VALUE()); + } + + $basis = (int) $basis; + if (($basis < 0) || ($basis > 4)) { + throw new Exception(Functions::NAN()); + } + + return $basis; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Constants.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Constants.php new file mode 100644 index 00000000..ba9d2389 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Constants.php @@ -0,0 +1,10 @@ += $maturity) { - throw new Exception(Functions::NAN()); - } - } - - private static function validateRate($rate): float - { - if (!is_numeric($rate)) { - throw new Exception(Functions::VALUE()); - } - - $rate = (float) $rate; - if ($rate < 0.0) { - throw new Exception(Functions::NAN()); - } - - return $rate; - } - - private static function validateYield($yield): float - { - if (!is_numeric($yield)) { - throw new Exception(Functions::VALUE()); - } - - $yield = (float) $yield; - if ($yield < 0.0) { - throw new Exception(Functions::NAN()); - } - - return $yield; - } - - private static function validateRedemption($redemption): float - { - if (!is_numeric($redemption)) { - throw new Exception(Functions::VALUE()); - } - - $redemption = (float) $redemption; - if ($redemption <= 0.0) { - throw new Exception(Functions::NAN()); - } - - return $redemption; - } - - private static function validateDiscount($discount): float - { - if (!is_numeric($discount)) { - throw new Exception(Functions::VALUE()); - } - - $discount = (float) $discount; - if ($discount <= 0.0) { - throw new Exception(Functions::NAN()); - } - - return $discount; - } - - private static function validateFrequency($frequency): int - { - if (!is_numeric($frequency)) { - throw new Exception(Functions::VALUE()); - } - - $frequency = (int) $frequency; - if ( - ($frequency !== self::FREQUENCY_ANNUAL) && - ($frequency !== self::FREQUENCY_SEMI_ANNUAL) && - ($frequency !== self::FREQUENCY_QUARTERLY) - ) { - throw new Exception(Functions::NAN()); - } - - return $frequency; - } - - private static function validateBasis($basis): int - { - if (!is_numeric($basis)) { - throw new Exception(Functions::VALUE()); - } - - $basis = (int) $basis; - if (($basis < 0) || ($basis > 4)) { - throw new Exception(Functions::NAN()); - } - - return $basis; - } } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php new file mode 100644 index 00000000..0918d637 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php @@ -0,0 +1,136 @@ +getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + if (!is_numeric($daysPerYear)) { + return $daysPerYear; + } + $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return $daysBetweenSettlementAndMaturity; + } + $daysBetweenSettlementAndMaturity *= $daysPerYear; + + return (($redemption - $price) / $price) * ($daysPerYear / $daysBetweenSettlementAndMaturity); + } + + /** + * YIELDMAT. + * + * Returns the annual yield of a security that pays interest at maturity. + * + * @param mixed $settlement The security's settlement date. + * The security's settlement date is the date after the issue date when the security + * is traded to the buyer. + * @param mixed $maturity The security's maturity date. + * The maturity date is the date when the security expires. + * @param mixed $issue The security's issue date + * @param int $rate The security's interest rate at date of issue + * @param int $price The security's price per $100 face value + * @param int $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * + * @return float|string Result, or a string containing an error + */ + public static function yieldAtMaturity($settlement, $maturity, $issue, $rate, $price, $basis = 0) + { + $settlement = Functions::flattenSingleValue($settlement); + $maturity = Functions::flattenSingleValue($maturity); + $issue = Functions::flattenSingleValue($issue); + $rate = Functions::flattenSingleValue($rate); + $price = Functions::flattenSingleValue($price); + $basis = Functions::flattenSingleValue($basis); + + try { + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); + self::validateSecurityPeriod($settlement, $maturity); + $issue = self::validateIssueDate($issue); + $rate = self::validateRate($rate); + $price = self::validatePrice($price); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + if (!is_numeric($daysPerYear)) { + return $daysPerYear; + } + $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return $daysBetweenIssueAndSettlement; + } + $daysBetweenIssueAndSettlement *= $daysPerYear; + $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); + if (!is_numeric($daysBetweenIssueAndMaturity)) { + // return date error + return $daysBetweenIssueAndMaturity; + } + $daysBetweenIssueAndMaturity *= $daysPerYear; + $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + if (!is_numeric($daysBetweenSettlementAndMaturity)) { + // return date error + return $daysBetweenSettlementAndMaturity; + } + $daysBetweenSettlementAndMaturity *= $daysPerYear; + + return ((1 + (($daysBetweenIssueAndMaturity / $daysPerYear) * $rate) - (($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) / + (($price / 100) + (($daysBetweenIssueAndSettlement / $daysPerYear) * $rate))) * + ($daysPerYear / $daysBetweenSettlementAndMaturity); + } +} diff --git a/tests/data/Calculation/Financial/AMORDEGRC.php b/tests/data/Calculation/Financial/AMORDEGRC.php index 59549e78..f4007033 100644 --- a/tests/data/Calculation/Financial/AMORDEGRC.php +++ b/tests/data/Calculation/Financial/AMORDEGRC.php @@ -7,6 +7,30 @@ return [ 776, 2400, '2008-08-19', '2008-12-31', 300, 1, 0.15, 1, ], + [ + 820, + 2400, '2008-08-19', '2008-12-31', 300, 1, 0.2, 1, + ], + [ + 492, + 2400, '2008-08-19', '2008-12-31', 300, 2, 0.2, 1, + ], + [ + 886, + 2400, '2008-08-19', '2008-12-31', 300, 1, 0.22, 1, + ], + [ + 949, + 2400, '2008-08-19', '2008-12-31', 300, 1, 0.24, 1, + ], + [ + 494, + 2400, '2008-08-19', '2008-12-31', 300, 2, 0.24, 1, + ], + [ + 902, + 2400, '2008-08-19', '2008-12-31', 300, 1, 0.3, 1, + ], [ 42, 150, '2011-01-01', '2011-09-30', 20, 1, 0.2, 4, @@ -27,4 +51,8 @@ return [ '#VALUE!', 550, 'notADate', '2020-12-25', 20, 1, 0.2, 4, ], + [ + '#VALUE!', + 550, '2011-01-01', 'notADate', 20, 1, 0.2, 4, + ], ]; diff --git a/tests/data/Calculation/Financial/AMORLINC.php b/tests/data/Calculation/Financial/AMORLINC.php index 46f19332..34485c8a 100644 --- a/tests/data/Calculation/Financial/AMORLINC.php +++ b/tests/data/Calculation/Financial/AMORLINC.php @@ -5,26 +5,30 @@ return [ [ 360, - 2400, '2008-08-19', '2008-12-31', 300, 1, 0.14999999999999999, 1, + 2400, '2008-08-19', '2008-12-31', 300, 1, 0.15, 1, + ], + [ + 576, + 2400, '2008-08-19', '2008-12-31', 300, 2, 0.24, 1, ], [ 30, - 150, '2011-01-01', '2011-09-30', 20, 1, 0.20000000000000001, 4, + 150, '2011-01-01', '2011-09-30', 20, 1, 0.2, 4, ], [ 22.41666667, - 150, '2011-01-01', '2011-09-30', 20, 0, 0.20000000000000001, 4, + 150, '2011-01-01', '2011-09-30', 20, 0, 0.2, 4, ], [ 17.58333333, - 150, '2011-01-01', '2011-09-30', 20, 4, 0.20000000000000001, 4, + 150, '2011-01-01', '2011-09-30', 20, 4, 0.2, 4, ], [ 0.0, - 150, '2011-01-01', '2011-09-30', 20, 5, 0.20000000000000001, 4, + 150, '2011-01-01', '2011-09-30', 20, 5, 0.2, 4, ], [ '#VALUE!', - 150, 'notADate', '2011-09-30', 20, 1, 0.20000000000000001, 4, + 150, 'notADate', '2011-09-30', 20, 1, 0.2, 4, ], ]; diff --git a/tests/data/Calculation/Financial/DB.php b/tests/data/Calculation/Financial/DB.php index 7f8fc6fa..89bd22f2 100644 --- a/tests/data/Calculation/Financial/DB.php +++ b/tests/data/Calculation/Financial/DB.php @@ -115,6 +115,47 @@ return [ 6, 6, ], + [ + 4651.199, + 12000, + 2000, + 3.5, + 2, + 1, + ], + [ + 3521.399, + 12000, + 2000, + 5, + 2.5, + 1, + ], + [ + 3521.399, + 12000, + 2000, + 5, + 2.5, + 1.2, + ], + // Period value between 0 and 1 not yet handled in code + // [ + // 301.0, + // 12000, + // 2000, + // 5, + // 0.5, + // 1, + // ], + [ + -554.116, + 12000, + 15000, + 5, + 2, + 1, + ], [ '#NUM!', -1000, @@ -125,10 +166,82 @@ return [ ], [ '#VALUE!', - 'ABC', - 100, + 'Invalid', + 1000, 5, - 6, - 6, + 2, + 1, + ], + [ + '#VALUE!', + 12000, + 'Invalid', + 5, + 2, + 1, + ], + [ + '#VALUE!', + 12000, + 1000, + 'Invalid', + 2, + 1, + ], + [ + '#VALUE!', + 12000, + 1000, + 5, + 'Invalid', + 1, + ], + [ + '#VALUE!', + 12000, + 1000, + 5, + 2, + 'Invalid', + ], + [ + '#NUM!', + -12000, + 1000, + 5, + 2, + 1, + ], + [ + '#NUM!', + 12000, + -1000, + 5, + 2, + 1, + ], + [ + '#NUM!', + 12000, + 1000, + 5, + 0, + 1, + ], + [ + '#NUM!', + 12000, + 1000, + 5, + -2, + 1, + ], + [ + '#NUM!', + 12000, + 1000, + 5, + 2, + 0, ], ]; diff --git a/tests/data/Calculation/Financial/DDB.php b/tests/data/Calculation/Financial/DDB.php index 879224ca..67962ea2 100644 --- a/tests/data/Calculation/Financial/DDB.php +++ b/tests/data/Calculation/Financial/DDB.php @@ -98,6 +98,47 @@ return [ 5, 5, ], + [ + 972.0, + 12000, + 1000, + 5, + 3, + 0.5, + ], + [ + 1259.4752186588921, + 12000, + 1000, + 3.5, + 3, + 0.5, + ], + [ + 1080.00, + 12000, + 1000, + 5, + 2, + 0.5, + ], + [ + 0.0, + 12000, + 15000, + 5, + 2, + 0.5, + ], + // Code does not yet handle fractional period values for DDB, only integer + // [ + // 1024.58, + // 12000, + // 1000, + // 5, + // 2.5, + // 0.5, + // ], [ '#NUM!', -2400, @@ -112,4 +153,76 @@ return [ 36500, 1, ], + [ + '#VALUE!', + 12000, + 'INVALID', + 5, + 3, + 0.5, + ], + [ + '#VALUE!', + 12000, + 1000, + 'INVALID', + 3, + 0.5, + ], + [ + '#VALUE!', + 12000, + 1000, + 5, + 'INVALID', + 0.5, + ], + [ + '#VALUE!', + 12000, + 1000, + 5, + 3, + 'INVALID', + ], + [ + '#NUM!', + 12000, + -1000, + 5, + 3, + 0.5, + ], + [ + '#NUM!', + 12000, + 1000, + 5, + -3, + 0.5, + ], + [ + '#NUM!', + 12000, + 1000, + 5, + 3, + -0.5, + ], + [ + '#NUM!', + 12000, + 1000, + 5, + 0, + 0.5, + ], + [ + '#NUM!', + 12000, + 1000, + 2, + 3, + 0.5, + ], ]; diff --git a/tests/data/Calculation/Financial/SLN.php b/tests/data/Calculation/Financial/SLN.php index 5dcf9eda..f63120c1 100644 --- a/tests/data/Calculation/Financial/SLN.php +++ b/tests/data/Calculation/Financial/SLN.php @@ -30,11 +30,39 @@ return [ [45000, 7500, 10], ], [ - '#NUM!', - [10000, 1000, -1], + -10500, + [12000, 1500, -1], + ], + [ + 21000, + [12000, 1500, 0.5], + ], + [ + 3250, + [12000, -1000, 4], + ], + [ + -250, + [0, 1000, 4], + ], + [ + -600, + [12000, 15000, 5], + ], + [ + '#DIV/0!', + [12000, 1500, 0], ], [ '#VALUE!', - ['INVALID', 1000, -1], + ['INVALID', 1000, 1], + ], + [ + '#VALUE!', + [12000, 'INVALID', 1], + ], + [ + '#VALUE!', + [12000, 1000, 'INVALID'], ], ]; diff --git a/tests/data/Calculation/Financial/SYD.php b/tests/data/Calculation/Financial/SYD.php index e6b612b6..a8dd078c 100644 --- a/tests/data/Calculation/Financial/SYD.php +++ b/tests/data/Calculation/Financial/SYD.php @@ -33,6 +33,30 @@ return [ 409.09090909090907, [30000, 7500, 10, 10], ], + [ + -800, + [-2000, 1000, 5, 2], + ], + [ + 3771.4285714285716, + [12000, 1000, 2.5, 2], + ], + [ + 5028.571428571428, + [12000, 1000, 2.5, 1.5], + ], + [ + -600, + [-2000, 1000, 5, 3], + ], + [ + -800, + [12000, 15000, 5, 2], + ], + [ + '#NUM!', + [12000, -1000, 5, 3], + ], [ '#NUM!', [10000, 1000, 5, 10], @@ -41,4 +65,32 @@ return [ '#VALUE!', ['INVALID', 1000, 5, 1], ], + [ + '#VALUE!', + [12000, 'INVALID', 5, 1], + ], + [ + '#VALUE!', + [12000, 1000, 'INVALID', 1], + ], + [ + '#VALUE!', + [12000, 1000, 5, 'INVALID'], + ], + [ + '#NUM!', + [12000, -1, 5, 2], + ], + [ + '#NUM!', + [12000, 1000, -5, 1], + ], + [ + '#NUM!', + [12000, 1000, 5, 0], + ], + [ + '#NUM!', + [12000, 1000, 5, -1], + ], ]; diff --git a/tests/data/Calculation/Financial/YIELDDISC.php b/tests/data/Calculation/Financial/YIELDDISC.php index e6260a86..8750e98a 100644 --- a/tests/data/Calculation/Financial/YIELDDISC.php +++ b/tests/data/Calculation/Financial/YIELDDISC.php @@ -9,4 +9,28 @@ return [ 0.06220123250590336, '1-Jan-2017', '30-Jun-2017', 97, 100, ], + [ + '#VALUE!', + 'Invalid', '30-Jun-2017', 97, 100, + ], + [ + '#VALUE!', + '1-Jan-2017', 'Invalid', 97, 100, + ], + [ + '#VALUE!', + '1-Jan-2017', '30-Jun-2017', 'NaN', 100, + ], + [ + '#VALUE!', + '1-Jan-2017', '30-Jun-2017', 97, 'NaN', + ], + [ + '#NUM!', + '1-Jan-2017', '30-Jun-2017', -97, 100, + ], + [ + '#NUM!', + '1-Jan-2017', '30-Jun-2017', 97, -100, + ], ]; diff --git a/tests/data/Calculation/Financial/YIELDMAT.php b/tests/data/Calculation/Financial/YIELDMAT.php index 6f26b15c..49ced033 100644 --- a/tests/data/Calculation/Financial/YIELDMAT.php +++ b/tests/data/Calculation/Financial/YIELDMAT.php @@ -9,4 +9,32 @@ return [ 0.04210977320221025, '1-Jan-2017', '30-Jun-2018', '01-Jul-2014', 0.055, 101, ], + [ + '#VALUE!', + 'Invalid', '30-Jun-2018', '01-Jul-2014', 0.055, 101, + ], + [ + '#VALUE!', + '1-Jan-2017', 'Invalid', '01-Jul-2014', 0.055, 101, + ], + [ + '#VALUE!', + '1-Jan-2017', '30-Jun-2018', 'Invalid', 0.055, 101, + ], + [ + '#VALUE!', + '1-Jan-2017', '30-Jun-2018', '01-Jul-2014', 'NaN', 101, + ], + [ + '#VALUE!', + '1-Jan-2017', '30-Jun-2018', '01-Jul-2014', 0.055, 'NaN', + ], + [ + '#NUM!', + '1-Jan-2017', '30-Jun-2018', '01-Jul-2014', -0.055, 101, + ], + [ + '#NUM!', + '1-Jan-2017', '30-Jun-2018', '01-Jul-2014', 0.055, -101, + ], ]; From 1a7b9a446abab5499bbcdd96349663b6a819909d Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 23 Mar 2021 13:34:28 +0100 Subject: [PATCH 58/89] First phase of refactoring the Excel Text functions (#1945) * Refactoring the Excel Text functions * More unit tests for utf-8 handling, for edge cases, and for argument validations --- .../Calculation/Calculation.php | 68 +-- src/PhpSpreadsheet/Calculation/TextData.php | 503 +++++------------- .../Calculation/TextData/CaseConvert.php | 64 +++ .../Calculation/TextData/CharacterConvert.php | 69 +++ .../Calculation/TextData/Concatenate.php | 82 +++ .../Calculation/TextData/Extract.php | 77 +++ .../Calculation/TextData/Format.php | 196 +++++++ .../Calculation/TextData/Replace.php | 65 +++ .../Calculation/TextData/Search.php | 80 +++ .../Calculation/TextData/Text.php | 59 ++ .../Calculation/TextData/Trim.php | 58 ++ .../Functions/TextData/LeftTest.php | 42 ++ .../Functions/TextData/LowerTest.php | 41 ++ .../Functions/TextData/MidTest.php | 43 ++ .../Functions/TextData/ProperTest.php | 41 ++ .../Functions/TextData/ReptTest.php | 20 +- .../Functions/TextData/RightTest.php | 42 ++ .../Functions/TextData/UpperTest.php | 41 ++ tests/data/Calculation/TextData/CLEAN.php | 4 + tests/data/Calculation/TextData/FIND.php | 30 ++ tests/data/Calculation/TextData/LEFT.php | 20 + tests/data/Calculation/TextData/LOWER.php | 12 + tests/data/Calculation/TextData/MID.php | 26 +- tests/data/Calculation/TextData/PROPER.php | 12 + tests/data/Calculation/TextData/REPLACE.php | 28 + tests/data/Calculation/TextData/REPT.php | 3 + tests/data/Calculation/TextData/RIGHT.php | 20 + tests/data/Calculation/TextData/SEARCH.php | 30 ++ .../data/Calculation/TextData/SUBSTITUTE.php | 34 ++ tests/data/Calculation/TextData/TEXTJOIN.php | 12 + tests/data/Calculation/TextData/UPPER.php | 12 + 31 files changed, 1424 insertions(+), 410 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/Concatenate.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/Extract.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/Format.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/Replace.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/Search.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/Text.php create mode 100644 src/PhpSpreadsheet/Calculation/TextData/Trim.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 9317699b..c5dbaa53 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -483,7 +483,7 @@ class Calculation ], 'CHAR' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'CHARACTER'], + 'functionCall' => [TextData\CharacterConvert::class, 'character'], 'argumentCount' => '1', ], 'CHIDIST' => [ @@ -533,12 +533,12 @@ class Calculation ], 'CLEAN' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'TRIMNONPRINTABLE'], + 'functionCall' => [TextData\Trim::class, 'nonPrintable'], 'argumentCount' => '1', ], 'CODE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'ASCIICODE'], + 'functionCall' => [TextData\CharacterConvert::class, 'code'], 'argumentCount' => '1', ], 'COLUMN' => [ @@ -570,12 +570,12 @@ class Calculation ], 'CONCAT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'CONCATENATE'], + 'functionCall' => [TextData\Concatenate::class, 'CONCATENATE'], 'argumentCount' => '1+', ], 'CONCATENATE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'CONCATENATE'], + 'functionCall' => [TextData\Concatenate::class, 'CONCATENATE'], 'argumentCount' => '1+', ], 'CONFIDENCE' => [ @@ -870,7 +870,7 @@ class Calculation ], 'DOLLAR' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'DOLLAR'], + 'functionCall' => [TextData\Format::class, 'DOLLAR'], 'argumentCount' => '1,2', ], 'DOLLARDE' => [ @@ -970,7 +970,7 @@ class Calculation ], 'EXACT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'EXACT'], + 'functionCall' => [TextData\Text::class, 'exact'], 'argumentCount' => '2', ], 'EXP' => [ @@ -1030,12 +1030,12 @@ class Calculation ], 'FIND' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'SEARCHSENSITIVE'], + 'functionCall' => [TextData\Search::class, 'sensitive'], 'argumentCount' => '2,3', ], 'FINDB' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'SEARCHSENSITIVE'], + 'functionCall' => [TextData\Search::class, 'sensitive'], 'argumentCount' => '2,3', ], 'FINV' => [ @@ -1065,7 +1065,7 @@ class Calculation ], 'FIXED' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'FIXEDFORMAT'], + 'functionCall' => [TextData\Format::class, 'FIXEDFORMAT'], 'argumentCount' => '1-3', ], 'FLOOR' => [ @@ -1541,22 +1541,22 @@ class Calculation ], 'LEFT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'LEFT'], + 'functionCall' => [TextData\Extract::class, 'left'], 'argumentCount' => '1,2', ], 'LEFTB' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'LEFT'], + 'functionCall' => [TextData\Extract::class, 'left'], 'argumentCount' => '1,2', ], 'LEN' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'STRINGLENGTH'], + 'functionCall' => [TextData\Text::class, 'length'], 'argumentCount' => '1', ], 'LENB' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'STRINGLENGTH'], + 'functionCall' => [TextData\Text::class, 'length'], 'argumentCount' => '1', ], 'LINEST' => [ @@ -1611,7 +1611,7 @@ class Calculation ], 'LOWER' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'LOWERCASE'], + 'functionCall' => [TextData\CaseConvert::class, 'lower'], 'argumentCount' => '1', ], 'MATCH' => [ @@ -1656,12 +1656,12 @@ class Calculation ], 'MID' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'MID'], + 'functionCall' => [TextData\Extract::class, 'mid'], 'argumentCount' => '3', ], 'MIDB' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'MID'], + 'functionCall' => [TextData\Extract::class, 'mid'], 'argumentCount' => '3', ], 'MIN' => [ @@ -1836,7 +1836,7 @@ class Calculation ], 'NUMBERVALUE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'NUMBERVALUE'], + 'functionCall' => [TextData\Format::class, 'NUMBERVALUE'], 'argumentCount' => '1+', ], 'OCT2BIN' => [ @@ -2008,7 +2008,7 @@ class Calculation ], 'PROPER' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'PROPERCASE'], + 'functionCall' => [TextData\CaseConvert::class, 'proper'], 'argumentCount' => '1', ], 'PV' => [ @@ -2083,27 +2083,27 @@ class Calculation ], 'REPLACE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'REPLACE'], + 'functionCall' => [TextData\Replace::class, 'replace'], 'argumentCount' => '4', ], 'REPLACEB' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'REPLACE'], + 'functionCall' => [TextData\Replace::class, 'replace'], 'argumentCount' => '4', ], 'REPT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'builtinREPT'], + 'functionCall' => [TextData\Concatenate::class, 'builtinREPT'], 'argumentCount' => '2', ], 'RIGHT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'RIGHT'], + 'functionCall' => [TextData\Extract::class, 'right'], 'argumentCount' => '1,2', ], 'RIGHTB' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'RIGHT'], + 'functionCall' => [TextData\Extract::class, 'right'], 'argumentCount' => '1,2', ], 'ROMAN' => [ @@ -2155,12 +2155,12 @@ class Calculation ], 'SEARCH' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'], + 'functionCall' => [TextData\Search::class, 'insensitive'], 'argumentCount' => '2,3', ], 'SEARCHB' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'SEARCHINSENSITIVE'], + 'functionCall' => [TextData\Search::class, 'insensitive'], 'argumentCount' => '2,3', ], 'SEC' => [ @@ -2300,7 +2300,7 @@ class Calculation ], 'SUBSTITUTE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'SUBSTITUTE'], + 'functionCall' => [TextData\Replace::class, 'substitute'], 'argumentCount' => '3,4', ], 'SUBTOTAL' => [ @@ -2361,7 +2361,7 @@ class Calculation ], 'T' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'RETURNSTRING'], + 'functionCall' => [TextData\Text::class, 'test'], 'argumentCount' => '1', ], 'TAN' => [ @@ -2411,7 +2411,7 @@ class Calculation ], 'TEXT' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'TEXTFORMAT'], + 'functionCall' => [TextData\Format::class, 'TEXTFORMAT'], 'argumentCount' => '2', ], 'TEXTJOIN' => [ @@ -2461,7 +2461,7 @@ class Calculation ], 'TRIM' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'TRIMSPACES'], + 'functionCall' => [TextData\Trim::class, 'spaces'], 'argumentCount' => '1', ], 'TRIMMEAN' => [ @@ -2496,12 +2496,12 @@ class Calculation ], 'UNICHAR' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'CHARACTER'], + 'functionCall' => [TextData\CharacterConvert::class, 'character'], 'argumentCount' => '1', ], 'UNICODE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'ASCIICODE'], + 'functionCall' => [TextData\CharacterConvert::class, 'code'], 'argumentCount' => '1', ], 'UNIQUE' => [ @@ -2511,7 +2511,7 @@ class Calculation ], 'UPPER' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'UPPERCASE'], + 'functionCall' => [TextData\CaseConvert::class, 'upper'], 'argumentCount' => '1', ], 'USDOLLAR' => [ @@ -2521,7 +2521,7 @@ class Calculation ], 'VALUE' => [ 'category' => Category::CATEGORY_TEXT_AND_DATA, - 'functionCall' => [TextData::class, 'VALUE'], + 'functionCall' => [TextData\Format::class, 'VALUE'], 'argumentCount' => '1', ], 'VAR' => [ diff --git a/src/PhpSpreadsheet/Calculation/TextData.php b/src/PhpSpreadsheet/Calculation/TextData.php index b886ce08..c7e91a47 100644 --- a/src/PhpSpreadsheet/Calculation/TextData.php +++ b/src/PhpSpreadsheet/Calculation/TextData.php @@ -3,141 +3,88 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use DateTimeInterface; -use PhpOffice\PhpSpreadsheet\Shared\Date; -use PhpOffice\PhpSpreadsheet\Shared\StringHelper; -use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +/** + * @deprecated 1.18.0 + */ class TextData { - private static $invalidChars; - - private static function unicodeToOrd($character) - { - return unpack('V', iconv('UTF-8', 'UCS-4LE', $character))[1]; - } - /** * CHARACTER. * + * @Deprecated 1.18.0 + * + * @see Use the character() method in the TextData\CharacterConvert class instead + * * @param string $character Value * * @return string */ public static function CHARACTER($character) { - $character = Functions::flattenSingleValue($character); - - if (!is_numeric($character)) { - return Functions::VALUE(); - } - $character = (int) $character; - if ($character < 1 || $character > 255) { - return Functions::VALUE(); - } - - return iconv('UCS-4LE', 'UTF-8', pack('V', $character)); + return TextData\CharacterConvert::character($character); } /** * TRIMNONPRINTABLE. * + * @Deprecated 1.18.0 + * + * @see Use the nonPrintable() method in the TextData\Trim class instead + * * @param mixed $stringValue Value to check * * @return string */ public static function TRIMNONPRINTABLE($stringValue = '') { - $stringValue = Functions::flattenSingleValue($stringValue); - - if (is_bool($stringValue)) { - return ($stringValue) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - if (self::$invalidChars === null) { - self::$invalidChars = range(chr(0), chr(31)); - } - - if (is_string($stringValue) || is_numeric($stringValue)) { - return str_replace(self::$invalidChars, '', trim($stringValue, "\x00..\x1F")); - } - - return null; + return TextData\Trim::nonPrintable($stringValue); } /** * TRIMSPACES. * + * @Deprecated 1.18.0 + * + * @see Use the spaces() method in the TextData\Trim class instead + * * @param mixed $stringValue Value to check * * @return string */ public static function TRIMSPACES($stringValue = '') { - $stringValue = Functions::flattenSingleValue($stringValue); - if (is_bool($stringValue)) { - return ($stringValue) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - if (is_string($stringValue) || is_numeric($stringValue)) { - return trim(preg_replace('/ +/', ' ', trim($stringValue, ' ')), ' '); - } - - return null; - } - - private static function convertBooleanValue($value) - { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE) { - return (int) $value; - } - - return ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); + return TextData\Trim::spaces($stringValue); } /** * ASCIICODE. * + * @Deprecated 1.18.0 + * + * @see Use the code() method in the TextData\CharacterConvert class instead + * * @param string $characters Value * * @return int|string A string if arguments are invalid */ public static function ASCIICODE($characters) { - if (($characters === null) || ($characters === '')) { - return Functions::VALUE(); - } - $characters = Functions::flattenSingleValue($characters); - if (is_bool($characters)) { - $characters = self::convertBooleanValue($characters); - } - - $character = $characters; - if (mb_strlen($characters, 'UTF-8') > 1) { - $character = mb_substr($characters, 0, 1, 'UTF-8'); - } - - return self::unicodeToOrd($character); + return TextData\CharacterConvert::code($characters); } /** * CONCATENATE. * + * @Deprecated 1.18.0 + * + * @see Use the CONCATENATE() method in the TextData\Concatenate class instead + * * @return string */ public static function CONCATENATE(...$args) { - $returnValue = ''; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $arg) { - if (is_bool($arg)) { - $arg = self::convertBooleanValue($arg); - } - $returnValue .= $arg; - } - - return $returnValue; + return TextData\Concatenate::CONCATENATE(...$args); } /** @@ -146,6 +93,10 @@ class TextData * 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).. * + * @Deprecated 1.18.0 + * + * @see Use the DOLLAR() method in the TextData\Format class instead + * * @param float $value The value to format * @param int $decimals The number of digits to display to the right of the decimal point. * If decimals is negative, number is rounded to the left of the decimal point. @@ -155,33 +106,16 @@ class TextData */ public static function DOLLAR($value = 0, $decimals = 2) { - $value = Functions::flattenSingleValue($value); - $decimals = $decimals === null ? 0 : Functions::flattenSingleValue($decimals); - - // Validate parameters - if (!is_numeric($value) || !is_numeric($decimals)) { - return Functions::VALUE(); - } - $decimals = (int) $decimals; - - $mask = '$#,##0'; - if ($decimals > 0) { - $mask .= '.' . str_repeat('0', $decimals); - } else { - $round = 10 ** abs($decimals); - if ($value < 0) { - $round = 0 - $round; - } - $value = MathTrig\Mround::funcMround($value, $round); - } - $mask = "$mask;($mask)"; - - return NumberFormat::toFormattedString($value, $mask); + return TextData\Format::DOLLAR($value, $decimals); } /** * SEARCHSENSITIVE. * + * @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 @@ -190,33 +124,16 @@ class TextData */ public static function SEARCHSENSITIVE($needle, $haystack, $offset = 1) { - $needle = Functions::flattenSingleValue($needle); - $haystack = Functions::flattenSingleValue($haystack); - $offset = Functions::flattenSingleValue($offset); - - if (!is_bool($needle)) { - if (is_bool($haystack)) { - $haystack = ($haystack) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - if (($offset > 0) && (StringHelper::countCharacters($haystack) > $offset)) { - if (StringHelper::countCharacters($needle) === 0) { - return $offset; - } - - $pos = mb_strpos($haystack, $needle, --$offset, 'UTF-8'); - if ($pos !== false) { - return ++$pos; - } - } - } - - return Functions::VALUE(); + return TextData\Search::sensitive($needle, $haystack, $offset); } /** * SEARCHINSENSITIVE. * + * @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 @@ -225,33 +142,16 @@ class TextData */ public static function SEARCHINSENSITIVE($needle, $haystack, $offset = 1) { - $needle = Functions::flattenSingleValue($needle); - $haystack = Functions::flattenSingleValue($haystack); - $offset = Functions::flattenSingleValue($offset); - - if (!is_bool($needle)) { - if (is_bool($haystack)) { - $haystack = ($haystack) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - if (($offset > 0) && (StringHelper::countCharacters($haystack) > $offset)) { - if (StringHelper::countCharacters($needle) === 0) { - return $offset; - } - - $pos = mb_stripos($haystack, $needle, --$offset, 'UTF-8'); - if ($pos !== false) { - return ++$pos; - } - } - } - - return Functions::VALUE(); + return TextData\Search::insensitive($needle, $haystack, $offset); } /** * FIXEDFORMAT. * + * @Deprecated 1.18.0 + * + * @see Use the FIXEDFORMAT() method in the TextData\Format class instead + * * @param mixed $value Value to check * @param int $decimals * @param bool $no_commas @@ -260,35 +160,16 @@ class TextData */ public static function FIXEDFORMAT($value, $decimals = 2, $no_commas = false) { - $value = Functions::flattenSingleValue($value); - $decimals = Functions::flattenSingleValue($decimals); - $no_commas = Functions::flattenSingleValue($no_commas); - - // Validate parameters - if (!is_numeric($value) || !is_numeric($decimals)) { - return Functions::VALUE(); - } - $decimals = (int) floor($decimals); - - $valueResult = round($value, $decimals); - if ($decimals < 0) { - $decimals = 0; - } - if (!$no_commas) { - $valueResult = number_format( - $valueResult, - $decimals, - StringHelper::getDecimalSeparator(), - StringHelper::getThousandsSeparator() - ); - } - - return (string) $valueResult; + return TextData\Format::FIXEDFORMAT($value, $decimals, $no_commas); } /** * LEFT. * + * @Deprecated 1.18.0 + * + * @see Use the left() method in the TextData\Extract class instead + * * @param string $value Value * @param int $chars Number of characters * @@ -296,23 +177,16 @@ class TextData */ public static function LEFT($value = '', $chars = 1) { - $value = Functions::flattenSingleValue($value); - $chars = Functions::flattenSingleValue($chars); - - if (!is_numeric($chars) || $chars < 0) { - return Functions::VALUE(); - } - - if (is_bool($value)) { - $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - return mb_substr($value, 0, $chars, 'UTF-8'); + return TextData\Extract::left($value, $chars); } /** * MID. * + * @Deprecated 1.18.0 + * + * @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 @@ -321,24 +195,16 @@ class TextData */ public static function MID($value = '', $start = 1, $chars = null) { - $value = Functions::flattenSingleValue($value); - $start = Functions::flattenSingleValue($start); - $chars = Functions::flattenSingleValue($chars); - - if (!is_numeric($start) || $start < 1 || !is_numeric($chars) || $chars < 0) { - return Functions::VALUE(); - } - - if (is_bool($value)) { - $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - return mb_substr($value, --$start, $chars, 'UTF-8'); + return TextData\Extract::mid($value, $start, $chars); } /** * RIGHT. * + * @Deprecated 1.18.0 + * + * @see Use the right() method in the TextData\Extract class instead + * * @param string $value Value * @param int $chars Number of characters * @@ -346,36 +212,23 @@ class TextData */ public static function RIGHT($value = '', $chars = 1) { - $value = Functions::flattenSingleValue($value); - $chars = Functions::flattenSingleValue($chars); - - if (!is_numeric($chars) || $chars < 0) { - return Functions::VALUE(); - } - - if (is_bool($value)) { - $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - return mb_substr($value, mb_strlen($value, 'UTF-8') - $chars, $chars, 'UTF-8'); + return TextData\Extract::right($value, $chars); } /** * STRINGLENGTH. * + * @Deprecated 1.18.0 + * + * @see Use the length() method in the TextData\Text class instead + * * @param string $value Value * * @return int */ public static function STRINGLENGTH($value = '') { - $value = Functions::flattenSingleValue($value); - - if (is_bool($value)) { - $value = ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - return mb_strlen($value, 'UTF-8'); + return TextData\Text::length($value); } /** @@ -383,19 +236,17 @@ class TextData * * Converts a string value to upper case. * + * @Deprecated 1.18.0 + * + * @see Use the lower() method in the TextData\CaseConvert class instead + * * @param string $mixedCaseString * * @return string */ public static function LOWERCASE($mixedCaseString) { - $mixedCaseString = Functions::flattenSingleValue($mixedCaseString); - - if (is_bool($mixedCaseString)) { - $mixedCaseString = ($mixedCaseString) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - return StringHelper::strToLower($mixedCaseString); + return TextData\CaseConvert::lower($mixedCaseString); } /** @@ -403,19 +254,17 @@ class TextData * * Converts a string value to upper case. * + * @Deprecated 1.18.0 + * + * @see Use the upper() method in the TextData\CaseConvert class instead + * * @param string $mixedCaseString * * @return string */ public static function UPPERCASE($mixedCaseString) { - $mixedCaseString = Functions::flattenSingleValue($mixedCaseString); - - if (is_bool($mixedCaseString)) { - $mixedCaseString = ($mixedCaseString) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - return StringHelper::strToUpper($mixedCaseString); + return TextData\CaseConvert::upper($mixedCaseString); } /** @@ -423,24 +272,26 @@ class TextData * * Converts a string value to upper case. * + * @Deprecated 1.18.0 + * + * @see Use the proper() method in the TextData\CaseConvert class instead + * * @param string $mixedCaseString * * @return string */ public static function PROPERCASE($mixedCaseString) { - $mixedCaseString = Functions::flattenSingleValue($mixedCaseString); - - if (is_bool($mixedCaseString)) { - $mixedCaseString = ($mixedCaseString) ? Calculation::getTRUE() : Calculation::getFALSE(); - } - - return StringHelper::strToTitle($mixedCaseString); + return TextData\CaseConvert::proper($mixedCaseString); } /** * REPLACE. * + * @Deprecated 1.18.0 + * + * @see Use the replace() method in the TextData\Replace class instead + * * @param string $oldText String to modify * @param int $start Start character * @param int $chars Number of characters @@ -450,20 +301,16 @@ class TextData */ public static function REPLACE($oldText, $start, $chars, $newText) { - $oldText = Functions::flattenSingleValue($oldText); - $start = Functions::flattenSingleValue($start); - $chars = Functions::flattenSingleValue($chars); - $newText = Functions::flattenSingleValue($newText); - - $left = self::LEFT($oldText, $start - 1); - $right = self::RIGHT($oldText, self::STRINGLENGTH($oldText) - ($start + $chars) + 1); - - return $left . $newText . $right; + return TextData\Replace::replace($oldText, $start, $chars, $newText); } /** * SUBSTITUTE. * + * @Deprecated 1.18.0 + * + * @see Use the substitute() method in the TextData\Replace class instead + * * @param string $text Value * @param string $fromText From Value * @param string $toText To Value @@ -473,52 +320,32 @@ class TextData */ public static function SUBSTITUTE($text = '', $fromText = '', $toText = '', $instance = 0) { - $text = Functions::flattenSingleValue($text); - $fromText = Functions::flattenSingleValue($fromText); - $toText = Functions::flattenSingleValue($toText); - $instance = floor(Functions::flattenSingleValue($instance)); - - if ($instance == 0) { - return str_replace($fromText, $toText, $text); - } - - $pos = -1; - while ($instance > 0) { - $pos = mb_strpos($text, $fromText, $pos + 1, 'UTF-8'); - if ($pos === false) { - break; - } - --$instance; - } - - if ($pos !== false) { - return self::REPLACE($text, ++$pos, mb_strlen($fromText, 'UTF-8'), $toText); - } - - return $text; + return TextData\Replace::substitute($text, $fromText, $toText, $instance); } /** * RETURNSTRING. * + * @Deprecated 1.18.0 + * + * @see Use the test() method in the TextData\Text class instead + * * @param mixed $testValue Value to check * * @return null|string */ public static function RETURNSTRING($testValue = '') { - $testValue = Functions::flattenSingleValue($testValue); - - if (is_string($testValue)) { - return $testValue; - } - - return null; + return TextData\Text::test($testValue); } /** * TEXTFORMAT. * + * @Deprecated 1.18.0 + * + * @see Use the TEXTFORMAT() method in the TextData\Format class instead + * * @param mixed $value Value to check * @param string $format Format mask to use * @@ -526,65 +353,32 @@ class TextData */ public static function TEXTFORMAT($value, $format) { - $value = Functions::flattenSingleValue($value); - $format = Functions::flattenSingleValue($format); - - if ((is_string($value)) && (!is_numeric($value)) && Date::isDateTimeFormatCode($format)) { - $value = DateTime::DATEVALUE($value); - } - - return (string) NumberFormat::toFormattedString($value, $format); + return TextData\Format::TEXTFORMAT($value, $format); } /** * VALUE. * + * @Deprecated 1.18.0 + * + * @see Use the VALUE() method in the TextData\Format class instead + * * @param mixed $value Value to check * * @return DateTimeInterface|float|int|string A string if arguments are invalid */ public static function VALUE($value = '') { - $value = Functions::flattenSingleValue($value); - - if (!is_numeric($value)) { - $numberValue = str_replace( - StringHelper::getThousandsSeparator(), - '', - trim($value, " \t\n\r\0\x0B" . StringHelper::getCurrencyCode()) - ); - if (is_numeric($numberValue)) { - return (float) $numberValue; - } - - $dateSetting = Functions::getReturnDateType(); - Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); - - if (strpos($value, ':') !== false) { - $timeValue = DateTime::TIMEVALUE($value); - if ($timeValue !== Functions::VALUE()) { - Functions::setReturnDateType($dateSetting); - - return $timeValue; - } - } - $dateValue = DateTime::DATEVALUE($value); - if ($dateValue !== Functions::VALUE()) { - Functions::setReturnDateType($dateSetting); - - return $dateValue; - } - Functions::setReturnDateType($dateSetting); - - return Functions::VALUE(); - } - - return (float) $value; + return TextData\Format::VALUE($value); } /** * NUMBERVALUE. * + * @Deprecated 1.18.0 + * + * @see Use the NUMBERVALUE() method in the TextData\Format class instead + * * @param mixed $value Value to check * @param string $decimalSeparator decimal separator, defaults to locale defined value * @param string $groupSeparator group/thosands separator, defaults to locale defined value @@ -593,39 +387,7 @@ class TextData */ public static function NUMBERVALUE($value = '', $decimalSeparator = null, $groupSeparator = null) { - $value = Functions::flattenSingleValue($value); - $decimalSeparator = Functions::flattenSingleValue($decimalSeparator); - $groupSeparator = Functions::flattenSingleValue($groupSeparator); - - if (!is_numeric($value)) { - $decimalSeparator = empty($decimalSeparator) ? StringHelper::getDecimalSeparator() : $decimalSeparator; - $groupSeparator = empty($groupSeparator) ? StringHelper::getThousandsSeparator() : $groupSeparator; - - $decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator) . '/', $value, $matches, PREG_OFFSET_CAPTURE); - if ($decimalPositions > 1) { - return Functions::VALUE(); - } - $decimalOffset = array_pop($matches[0])[1]; - if (strpos($value, $groupSeparator, $decimalOffset) !== false) { - return Functions::VALUE(); - } - - $value = str_replace([$groupSeparator, $decimalSeparator], ['', '.'], $value); - - // Handle the special case of trailing % signs - $percentageString = rtrim($value, '%'); - if (!is_numeric($percentageString)) { - return Functions::VALUE(); - } - - $percentageAdjustment = strlen($value) - strlen($percentageString); - if ($percentageAdjustment) { - $value = (float) $percentageString; - $value /= 10 ** ($percentageAdjustment * 2); - } - } - - return (float) $value; + return TextData\Format::NUMBERVALUE($value, $decimalSeparator, $groupSeparator); } /** @@ -633,6 +395,10 @@ class TextData * EXACT is case-sensitive but ignores formatting differences. * Use EXACT to test text being entered into a document. * + * @Deprecated 1.18.0 + * + * @see Use the exact() method in the TextData\Text class instead + * * @param $value1 * @param $value2 * @@ -640,15 +406,16 @@ class TextData */ public static function EXACT($value1, $value2) { - $value1 = Functions::flattenSingleValue($value1); - $value2 = Functions::flattenSingleValue($value2); - - return (string) $value2 === (string) $value1; + return TextData\Text::exact($value1, $value2); } /** * TEXTJOIN. * + * @Deprecated 1.18.0 + * + * @see Use the TEXTJOIN() method in the TextData\Concatenate class instead + * * @param mixed $delimiter * @param mixed $ignoreEmpty * @param mixed $args @@ -657,23 +424,17 @@ class TextData */ public static function TEXTJOIN($delimiter, $ignoreEmpty, ...$args) { - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $key => &$arg) { - if ($ignoreEmpty && trim($arg) == '') { - unset($aArgs[$key]); - } elseif (is_bool($arg)) { - $arg = self::convertBooleanValue($arg); - } - } - - return implode($delimiter, $aArgs); + return TextData\Concatenate::TEXTJOIN($delimiter, $ignoreEmpty, ...$args); } /** * REPT. * - * Returns the result of builtin function round after validating args. + * Returns the result of builtin function repeat after validating args. + * + * @Deprecated 1.18.0 + * + * @see Use the builtinREPT() method in the TextData\Concatenate class instead * * @param string $str Should be numeric * @param mixed $number Should be int @@ -682,12 +443,6 @@ class TextData */ public static function builtinREPT($str, $number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number) || $number < 0) { - return Functions::VALUE(); - } - - return str_repeat($str, $number); + return TextData\Concatenate::builtinREPT($str, $number); } } diff --git a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php new file mode 100644 index 00000000..2a275133 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php @@ -0,0 +1,64 @@ + 255) { + return Functions::VALUE(); + } + + return iconv('UCS-4LE', 'UTF-8', pack('V', $character)); + } + + /** + * ASCIICODE. + * + * @param string $characters Value + * + * @return int|string A string if arguments are invalid + */ + public static function code($characters) + { + if (($characters === null) || ($characters === '')) { + return Functions::VALUE(); + } + $characters = Functions::flattenSingleValue($characters); + if (is_bool($characters)) { + $characters = self::convertBooleanValue($characters); + } + + $character = $characters; + if (mb_strlen($characters, 'UTF-8') > 1) { + $character = mb_substr($characters, 0, 1, 'UTF-8'); + } + + return self::unicodeToOrd($character); + } + + private static function unicodeToOrd($character) + { + return unpack('V', iconv('UTF-8', 'UCS-4LE', $character))[1]; + } + + private static function convertBooleanValue($value) + { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE) { + return (int) $value; + } + + return ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php new file mode 100644 index 00000000..5780bb6e --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/TextData/Concatenate.php @@ -0,0 +1,82 @@ + &$arg) { + if ($ignoreEmpty === true && is_string($arg) && trim($arg) === '') { + unset($aArgs[$key]); + } elseif (is_bool($arg)) { + $arg = self::convertBooleanValue($arg); + } + } + + return implode($delimiter, $aArgs); + } + + /** + * REPT. + * + * Returns the result of builtin function round after validating args. + * + * @param mixed $stringValue The value to repeat + * @param mixed $repeatCount The number of times the string value should be repeated + */ + public static function builtinREPT($stringValue, $repeatCount): string + { + $repeatCount = Functions::flattenSingleValue($repeatCount); + + if (!is_numeric($repeatCount) || $repeatCount < 0) { + return Functions::VALUE(); + } + + if (is_bool($stringValue)) { + $stringValue = self::convertBooleanValue($stringValue); + } + + return str_repeat($stringValue, (int) $repeatCount); + } + + private static function convertBooleanValue($value) + { + if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) { + return (int) $value; + } + + return ($value) ? Calculation::getTRUE() : Calculation::getFALSE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php new file mode 100644 index 00000000..126d9f49 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -0,0 +1,77 @@ + 0) { + $mask .= '.' . str_repeat('0', $decimals); + } else { + $round = 10 ** abs($decimals); + if ($value < 0) { + $round = 0 - $round; + } + $value = MathTrig\Mround::funcMround($value, $round); + } + $mask = "$mask;($mask)"; + + return NumberFormat::toFormattedString($value, $mask); + } + + /** + * FIXEDFORMAT. + * + * @param mixed $value Value to check + * @param mixed $decimals + * @param bool $noCommas + */ + public static function FIXEDFORMAT($value, $decimals = 2, $noCommas = false): string + { + $value = Functions::flattenSingleValue($value); + $decimals = $decimals === null ? 2 : Functions::flattenSingleValue($decimals); + $noCommas = Functions::flattenSingleValue($noCommas); + + // Validate parameters + if (!is_numeric($value) || !is_numeric($decimals)) { + return Functions::VALUE(); + } + $decimals = (int) floor($decimals); + + $valueResult = round($value, $decimals); + if ($decimals < 0) { + $decimals = 0; + } + if (!$noCommas) { + $valueResult = number_format( + $valueResult, + $decimals, + StringHelper::getDecimalSeparator(), + StringHelper::getThousandsSeparator() + ); + } + + return (string) $valueResult; + } + + /** + * TEXTFORMAT. + * + * @param mixed $value Value to check + * @param string $format Format mask to use + */ + public static function TEXTFORMAT($value, $format): string + { + $value = Functions::flattenSingleValue($value); + $format = Functions::flattenSingleValue($format); + + if ((is_string($value)) && (!is_numeric($value)) && Date::isDateTimeFormatCode($format)) { + $value = DateTime::DATEVALUE($value); + } + + return (string) NumberFormat::toFormattedString($value, $format); + } + + /** + * VALUE. + * + * @param mixed $value Value to check + * + * @return DateTimeInterface|float|int|string A string if arguments are invalid + */ + public static function VALUE($value = '') + { + $value = Functions::flattenSingleValue($value); + + if (!is_numeric($value)) { + $numberValue = str_replace( + StringHelper::getThousandsSeparator(), + '', + trim($value, " \t\n\r\0\x0B" . StringHelper::getCurrencyCode()) + ); + if (is_numeric($numberValue)) { + return (float) $numberValue; + } + + $dateSetting = Functions::getReturnDateType(); + Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); + + if (strpos($value, ':') !== false) { + $timeValue = DateTime::TIMEVALUE($value); + if ($timeValue !== Functions::VALUE()) { + Functions::setReturnDateType($dateSetting); + + return $timeValue; + } + } + $dateValue = DateTime::DATEVALUE($value); + if ($dateValue !== Functions::VALUE()) { + Functions::setReturnDateType($dateSetting); + + return $dateValue; + } + Functions::setReturnDateType($dateSetting); + + return Functions::VALUE(); + } + + return (float) $value; + } + + /** + * NUMBERVALUE. + * + * @param mixed $value Value to check + * @param string $decimalSeparator decimal separator, defaults to locale defined value + * @param string $groupSeparator group/thosands separator, defaults to locale defined value + * + * @return float|string + */ + public static function NUMBERVALUE($value = '', $decimalSeparator = null, $groupSeparator = null) + { + $value = Functions::flattenSingleValue($value); + $decimalSeparator = Functions::flattenSingleValue($decimalSeparator); + $groupSeparator = Functions::flattenSingleValue($groupSeparator); + + if (!is_numeric($value)) { + $decimalSeparator = empty($decimalSeparator) ? StringHelper::getDecimalSeparator() : $decimalSeparator; + $groupSeparator = empty($groupSeparator) ? StringHelper::getThousandsSeparator() : $groupSeparator; + + $decimalPositions = preg_match_all('/' . preg_quote($decimalSeparator) . '/', $value, $matches, PREG_OFFSET_CAPTURE); + if ($decimalPositions > 1) { + return Functions::VALUE(); + } + $decimalOffset = array_pop($matches[0])[1]; + if (strpos($value, $groupSeparator, $decimalOffset) !== false) { + return Functions::VALUE(); + } + + $value = str_replace([$groupSeparator, $decimalSeparator], ['', '.'], $value); + + // Handle the special case of trailing % signs + $percentageString = rtrim($value, '%'); + if (!is_numeric($percentageString)) { + return Functions::VALUE(); + } + + $percentageAdjustment = strlen($value) - strlen($percentageString); + if ($percentageAdjustment) { + $value = (float) $percentageString; + $value /= 10 ** ($percentageAdjustment * 2); + } + } + + return (float) $value; + } +} diff --git a/src/PhpSpreadsheet/Calculation/TextData/Replace.php b/src/PhpSpreadsheet/Calculation/TextData/Replace.php new file mode 100644 index 00000000..9a849ba0 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/TextData/Replace.php @@ -0,0 +1,65 @@ + 0) { + $pos = mb_strpos($text, $fromText, $pos + 1, 'UTF-8'); + if ($pos === false) { + break; + } + --$instance; + } + + if ($pos !== false) { + return self::REPLACE($text, ++$pos, mb_strlen($fromText, 'UTF-8'), $toText); + } + + return $text; + } +} diff --git a/src/PhpSpreadsheet/Calculation/TextData/Search.php b/src/PhpSpreadsheet/Calculation/TextData/Search.php new file mode 100644 index 00000000..acbe6a24 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/TextData/Search.php @@ -0,0 +1,80 @@ + 0) && (StringHelper::countCharacters($haystack) > $offset)) { + if (StringHelper::countCharacters($needle) === 0) { + return $offset; + } + + $pos = mb_strpos($haystack, $needle, --$offset, 'UTF-8'); + if ($pos !== false) { + return ++$pos; + } + } + } + + return Functions::VALUE(); + } + + /** + * SEARCHINSENSITIVE. + * + * @param string $needle The string to look for + * @param string $haystack The string in which to look + * @param int $offset Offset within $haystack + * + * @return int|string + */ + public static function insensitive($needle, $haystack, $offset = 1) + { + $needle = Functions::flattenSingleValue($needle); + $haystack = Functions::flattenSingleValue($haystack); + $offset = Functions::flattenSingleValue($offset); + + if (!is_bool($needle)) { + if (is_bool($haystack)) { + $haystack = ($haystack) ? Calculation::getTRUE() : Calculation::getFALSE(); + } + + if (($offset > 0) && (StringHelper::countCharacters($haystack) > $offset)) { + if (StringHelper::countCharacters($needle) === 0) { + return $offset; + } + + $pos = mb_stripos($haystack, $needle, --$offset, 'UTF-8'); + if ($pos !== false) { + return ++$pos; + } + } + } + + return Functions::VALUE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php new file mode 100644 index 00000000..a47d373b --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -0,0 +1,59 @@ +expectException(CalcExp::class); @@ -24,6 +39,9 @@ class ReptTest extends TestCase $this->expectException(CalcExp::class); $formula = "=REPT($val)"; } else { + if (is_bool($val)) { + $val = ($val) ? Calculation::getTRUE() : Calculation::getFALSE(); + } $formula = "=REPT($val, $rpt)"; } $spreadsheet = new Spreadsheet(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php index 50fc86dc..da4c7491 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php @@ -3,10 +3,16 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; use PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Settings; use PHPUnit\Framework\TestCase; class RightTest extends TestCase { + protected function tearDown(): void + { + Settings::setLocale('en_US'); + } + /** * @dataProvider providerRIGHT * @@ -22,4 +28,40 @@ class RightTest extends TestCase { return require 'tests/data/Calculation/TextData/RIGHT.php'; } + + /** + * @dataProvider providerLocaleRIGHT + * + * @param string $expectedResult + * @param $value + * @param mixed $locale + * @param mixed $characters + */ + public function testLowerWithLocaleBoolean($expectedResult, $locale, $value, $characters): void + { + $newLocale = Settings::setLocale($locale); + if ($newLocale === false) { + Settings::setLocale('en_US'); + self::markTestSkipped('Unable to set locale for locale-specific test'); + } + + $result = TextData::RIGHT($value, $characters); + self::assertEquals($expectedResult, $result); + + Settings::setLocale('en_US'); + } + + public function providerLocaleRIGHT() + { + return [ + ['RAI', 'fr_FR', true, 3], + ['AAR', 'nl_NL', true, 3], + ['OSI', 'fi', true, 3], + ['ИНА', 'bg', true, 3], + ['UX', 'fr_FR', false, 2], + ['WAAR', 'nl_NL', false, 4], + ['ÄTOSI', 'fi', false, 5], + ['ЖЬ', 'bg', false, 2], + ]; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php index 13fb0b86..cf2d569d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php @@ -3,10 +3,16 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\TextData; use PhpOffice\PhpSpreadsheet\Calculation\TextData; +use PhpOffice\PhpSpreadsheet\Settings; use PHPUnit\Framework\TestCase; class UpperTest extends TestCase { + protected function tearDown(): void + { + Settings::setLocale('en_US'); + } + /** * @dataProvider providerUPPER * @@ -23,4 +29,39 @@ class UpperTest extends TestCase { return require 'tests/data/Calculation/TextData/UPPER.php'; } + + /** + * @dataProvider providerLocaleLOWER + * + * @param string $expectedResult + * @param $value + * @param mixed $locale + */ + public function testLowerWithLocaleBoolean($expectedResult, $locale, $value): void + { + $newLocale = Settings::setLocale($locale); + if ($newLocale === false) { + Settings::setLocale('en_US'); + self::markTestSkipped('Unable to set locale for locale-specific test'); + } + + $result = TextData::UPPERCASE($value); + self::assertEquals($expectedResult, $result); + + Settings::setLocale('en_US'); + } + + public function providerLocaleLOWER() + { + return [ + ['VRAI', 'fr_FR', true], + ['WAAR', 'nl_NL', true], + ['TOSI', 'fi', true], + ['ИСТИНА', 'bg', true], + ['FAUX', 'fr_FR', false], + ['ONWAAR', 'nl_NL', false], + ['EPÄTOSI', 'fi', false], + ['ЛОЖЬ', 'bg', false], + ]; + } } diff --git a/tests/data/Calculation/TextData/CLEAN.php b/tests/data/Calculation/TextData/CLEAN.php index aab0fe3a..67608883 100644 --- a/tests/data/Calculation/TextData/CLEAN.php +++ b/tests/data/Calculation/TextData/CLEAN.php @@ -5,6 +5,10 @@ return [ 'HELLO ', 'HELLO ', ], + [ + ' HELLO ', + ' HELLO ', + ], [ 'HELLO', ' HELLO', diff --git a/tests/data/Calculation/TextData/FIND.php b/tests/data/Calculation/TextData/FIND.php index 7420841a..0a583456 100644 --- a/tests/data/Calculation/TextData/FIND.php +++ b/tests/data/Calculation/TextData/FIND.php @@ -31,6 +31,36 @@ return [ 'A', 'MARK BAKER', ], + [ + 1, + 'Ενα', + 'Ενα δύο τρία τέσσερα πέντε', + ], + [ + 9, + 'τρία', + 'Ενα δύο τρία τέσσερα πέντε', + ], + [ + 22, + 'πέντε', + 'Ενα δύο τρία τέσσερα πέντε', + ], + [ + 1, + 'ΕΝΑ', + 'ΕΝΑ ΔΎΟ ΤΡΊΑ ΤΈΣΣΕΡΑ ΠΈΝΤΕ', + ], + [ + 9, + 'ΤΡΊΑ', + 'ΕΝΑ ΔΎΟ ΤΡΊΑ ΤΈΣΣΕΡΑ ΠΈΝΤΕ', + ], + [ + 22, + 'ΠΈΝΤΕ', + 'ΕΝΑ ΔΎΟ ΤΡΊΑ ΤΈΣΣΕΡΑ ΠΈΝΤΕ', + ], [ 2, 'a', diff --git a/tests/data/Calculation/TextData/LEFT.php b/tests/data/Calculation/TextData/LEFT.php index 96702f6b..d524dc36 100644 --- a/tests/data/Calculation/TextData/LEFT.php +++ b/tests/data/Calculation/TextData/LEFT.php @@ -11,6 +11,11 @@ return [ '', 1, ], + [ + '', + 'ABC', + 0, + ], [ '#VALUE!', 'QWERTYUIOP', @@ -31,6 +36,21 @@ return [ 'ABCDEFGHI', 3, ], + [ + 'Ενα', + 'Ενα δύο τρία τέσσερα πέντε', + 3, + ], + [ + 'Ενα δύο', + 'Ενα δύο τρία τέσσερα πέντε', + 7, + ], + [ + 'Ενα δύο τρία', + 'Ενα δύο τρία τέσσερα πέντε', + 12, + ], [ 'TR', true, diff --git a/tests/data/Calculation/TextData/LOWER.php b/tests/data/Calculation/TextData/LOWER.php index 2a4064bf..c5360b95 100644 --- a/tests/data/Calculation/TextData/LOWER.php +++ b/tests/data/Calculation/TextData/LOWER.php @@ -9,6 +9,18 @@ return [ 'mark baker', 'MARK BAKER', ], + [ + 'buenos días', + 'BUENOS DÍAS', + ], + [ + 'καλημερα', + 'ΚΑΛΗΜΕΡΑ', + ], + [ + 'доброе утро', + 'ДОБРОЕ УТРО', + ], [ 'true', true, diff --git a/tests/data/Calculation/TextData/MID.php b/tests/data/Calculation/TextData/MID.php index 71d90e8b..b434f670 100644 --- a/tests/data/Calculation/TextData/MID.php +++ b/tests/data/Calculation/TextData/MID.php @@ -16,7 +16,7 @@ return [ [ '#VALUE!', 'QWERTYUIOP', - -1, + 0, 1, ], [ @@ -48,12 +48,36 @@ return [ 8, 20, ], + [ + '', + 'QWERTYUIOP', + 999, + 2, + ], [ 'DEF', 'ABCDEFGHI', 4, 3, ], + [ + 'δύο', + 'Ενα δύο τρία τέσσερα πέντε', + 5, + 3, + ], + [ + 'δύο τρία', + 'Ενα δύο τρία τέσσερα πέντε', + 5, + 8, + ], + [ + 'τρία τέσσερα', + 'Ενα δύο τρία τέσσερα πέντε', + 9, + 12, + ], [ 'R', true, diff --git a/tests/data/Calculation/TextData/PROPER.php b/tests/data/Calculation/TextData/PROPER.php index 84c29096..8bbf0e5c 100644 --- a/tests/data/Calculation/TextData/PROPER.php +++ b/tests/data/Calculation/TextData/PROPER.php @@ -5,6 +5,18 @@ return [ 'Mark Baker', 'MARK BAKER', ], + [ + 'Buenos Días', + 'BUENOS DÍAS', + ], + [ + 'Καλημερα', + 'ΚΑΛΗΜΕΡΑ', + ], + [ + 'Доброе Утро', + 'ДОБРОЕ УТРО', + ], [ 'True', true, diff --git a/tests/data/Calculation/TextData/REPLACE.php b/tests/data/Calculation/TextData/REPLACE.php index 086d1290..09e22968 100644 --- a/tests/data/Calculation/TextData/REPLACE.php +++ b/tests/data/Calculation/TextData/REPLACE.php @@ -29,4 +29,32 @@ return [ 0, 'DFG', ], + [ + 'Ενα δύοτρίατέσσεραπέντε', + 'Εναδύοτρίατέσσεραπέντε', + 4, + 0, + ' ', + ], + [ + 'Ενα δύο τρίατέσσεραπέντε', + 'Ενα δύοτρίατέσσεραπέντε', + 8, + 0, + ' ', + ], + [ + 'Ενα δύο τρία τέσσεραπέντε', + 'Ενα δύο τρίατέσσεραπέντε', + 13, + 0, + ' ', + ], + [ + 'Ενα δύο τρία τέσσερα πέντε', + 'Ενα δύο τρία τέσσεραπέντε', + 21, + 0, + ' ', + ], ]; diff --git a/tests/data/Calculation/TextData/REPT.php b/tests/data/Calculation/TextData/REPT.php index 24dd87e8..2c8d1c0d 100644 --- a/tests/data/Calculation/TextData/REPT.php +++ b/tests/data/Calculation/TextData/REPT.php @@ -7,5 +7,8 @@ return [ ['ABCABCABC', '"ABC"', 3], ['ABCABC', '"ABC"', 2.2], ['', '"ABC"', 0], + ['TRUETRUE', true, 2], + ['111', 1, 3], + ['δύο δύο ', '"δύο "', 2], ['#VALUE!', '"ABC"', -1], ]; diff --git a/tests/data/Calculation/TextData/RIGHT.php b/tests/data/Calculation/TextData/RIGHT.php index 95dfe96e..e6928df2 100644 --- a/tests/data/Calculation/TextData/RIGHT.php +++ b/tests/data/Calculation/TextData/RIGHT.php @@ -31,6 +31,26 @@ return [ 'ABCDEFGHI', 3, ], + [ + '', + 'ABCDEFGHI', + 0, + ], + [ + 'πέντε', + 'Ενα δύο τρία τέσσερα πέντε', + 5, + ], + [ + 'τέσσερα πέντε', + 'Ενα δύο τρία τέσσερα πέντε', + 13, + ], + [ + 'τρία τέσσερα πέντε', + 'Ενα δύο τρία τέσσερα πέντε', + 18, + ], [ 'UE', true, diff --git a/tests/data/Calculation/TextData/SEARCH.php b/tests/data/Calculation/TextData/SEARCH.php index 28cd98f8..579830f6 100644 --- a/tests/data/Calculation/TextData/SEARCH.php +++ b/tests/data/Calculation/TextData/SEARCH.php @@ -59,6 +59,36 @@ return [ '', 'Mark Baker', ], + [ + 1, + 'Ενα', + 'Ενα δύο τρία τέσσερα πέντε', + ], + [ + 9, + 'τρία', + 'Ενα δύο τρία τέσσερα πέντε', + ], + [ + 22, + 'πέντε', + 'Ενα δύο τρία τέσσερα πέντε', + ], + [ + 1, + 'Ενα', + 'ΕΝΑ ΔΥΟ ΤΡΙΑ ΤΕΣΣΕΡΑ ΠΕΝΤΕ', + ], + [ + 9, + 'τρία', + 'ΕΝΑ ΔΎΟ ΤΡΊΑ ΤΈΣΣΕΡΑ ΠΈΝΤΕ', + ], + [ + 22, + 'πέντε', + 'ΕΝΑ ΔΎΟ ΤΡΊΑ ΤΈΣΣΕΡΑ ΠΈΝΤΕ', + ], [ '#VALUE!', 'BITE', diff --git a/tests/data/Calculation/TextData/SUBSTITUTE.php b/tests/data/Calculation/TextData/SUBSTITUTE.php index 23f66a18..97cb8d0f 100644 --- a/tests/data/Calculation/TextData/SUBSTITUTE.php +++ b/tests/data/Calculation/TextData/SUBSTITUTE.php @@ -20,6 +20,20 @@ return [ 'x', 1, ], + [ + 'Mark Bxker', + 'Mark Baker', + 'a', + 'x', + 2, + ], + [ + 'Mark Bakker', + 'Mark Baker', + 'k', + 'kk', + 2, + ], [ 'Mark Baker', 'Mark Baker', @@ -27,6 +41,26 @@ return [ 'a', 1, ], + [ + 'Ενα δύο αρία αέσσερα πέναε', + 'Ενα δύο τρία τέσσερα πέντε', + 'τ', + 'α', + ], + [ + 'Ενα δύο τρία αέσσερα πέντε', + 'Ενα δύο τρία τέσσερα πέντε', + 'τ', + 'α', + 2, + ], + [ + 'Ενα δύο τρία ατέσσερα πέντε', + 'Ενα δύο τρία τέσσερα πέντε', + 'τ', + 'ατ', + 2, + ], 'Unicode equivalence is not supported' => [ "\u{0061}\u{030A}", "\u{0061}\u{030A}", diff --git a/tests/data/Calculation/TextData/TEXTJOIN.php b/tests/data/Calculation/TextData/TEXTJOIN.php index 9ad85e94..9c6b4246 100644 --- a/tests/data/Calculation/TextData/TEXTJOIN.php +++ b/tests/data/Calculation/TextData/TEXTJOIN.php @@ -5,10 +5,22 @@ return [ 'ABCDE,FGHIJ', [',', true, 'ABCDE', 'FGHIJ'], ], + [ + 'ABCDEFGHIJ', + ['', true, 'ABCDE', 'FGHIJ'], + ], [ '1-2-3', ['-', true, 1, 2, 3], ], + [ + '<<::>>', + ['::', true, '<<', '>>'], + ], + [ + 'Καλό απόγευμα', + [' ', true, 'Καλό', 'απόγευμα'], + ], [ 'Boolean-TRUE', ['-', true, 'Boolean', '', true], diff --git a/tests/data/Calculation/TextData/UPPER.php b/tests/data/Calculation/TextData/UPPER.php index b43163be..e5d2f18e 100644 --- a/tests/data/Calculation/TextData/UPPER.php +++ b/tests/data/Calculation/TextData/UPPER.php @@ -9,6 +9,18 @@ return [ 'MARK BAKER', 'mark baker', ], + [ + 'BUENOS DÍAS', + 'buenos días', + ], + [ + 'ΚΑΛΗΜΕΡΑ', + 'Καλημερα', + ], + [ + 'ДОБРОЕ УТРО', + 'доброе утро', + ], [ 'TRUE', true, From 07ad80075548344208a8a26d8d41d8b0a1789a61 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Wed, 24 Mar 2021 13:29:54 +0100 Subject: [PATCH 59/89] New Bessel Algorithm, providing a higher degree of accuracy and precision (#1946) * New Bessel Algorithm, providing a higher degree of precision (12 decimal places) and still matching/exceeding MS Excel's precision across the range of values --- .../Calculation/Engineering/BesselI.php | 106 +++- .../Calculation/Engineering/BesselJ.php | 141 ++++- .../Calculation/Engineering/BesselK.php | 60 ++- .../Calculation/Engineering/BesselY.php | 87 ++-- .../Functions/Engineering/BesselITest.php | 2 +- .../Functions/Engineering/BesselKTest.php | 2 +- .../Functions/Engineering/BesselYTest.php | 2 +- .../Functions/TextData/ReptTest.php | 6 +- .../data/Calculation/Engineering/BESSELI.php | 488 ++++++++++-------- .../data/Calculation/Engineering/BESSELJ.php | 360 +++++++++---- .../data/Calculation/Engineering/BESSELK.php | 252 +++++---- .../data/Calculation/Engineering/BESSELY.php | 213 +++++--- 12 files changed, 1078 insertions(+), 641 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php index eda5c12b..d39ac05c 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class BesselI { @@ -16,9 +15,12 @@ class BesselI * Excel Function: * BESSELI(x,ord) * - * @param float $x The value at which to evaluate the function. + * NOTE: The MS Excel implementation of the BESSELI function is still not accurate. + * This code provides a more accurate calculation + * + * @param mixed (float) $x The value at which to evaluate the function. * If x is nonnumeric, BESSELI returns the #VALUE! error value. - * @param int $ord The order of the Bessel function. + * @param mixed (int) $ord The 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. @@ -28,7 +30,7 @@ class BesselI public static function BESSELI($x, $ord) { $x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x); - $ord = ($ord === null) ? 0.0 : Functions::flattenSingleValue($ord); + $ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord); if ((is_numeric($x)) && (is_numeric($ord))) { $ord = (int) floor($ord); @@ -36,7 +38,7 @@ class BesselI return Functions::NAN(); } - $fResult = self::calculate($x, $ord); + $fResult = self::calculate((float) $x, $ord); return (is_nan($fResult)) ? Functions::NAN() : $fResult; } @@ -46,27 +48,87 @@ class BesselI private static function calculate(float $x, int $ord): float { - if (abs($x) <= 30) { - $fResult = $fTerm = ($x / 2) ** $ord / MathTrig::FACT($ord); - $ordK = 1; - $fSqrX = ($x * $x) / 4; - do { - $fTerm *= $fSqrX; - $fTerm /= ($ordK * ($ordK + $ord)); - $fResult += $fTerm; - } while ((abs($fTerm) > 1e-12) && (++$ordK < 100)); - - return $fResult; + // special cases + switch ($ord) { + case 0: + return self::besselI0($x); + case 1: + return self::besselI1($x); } - $f_2_PI = 2 * M_PI; + return self::besselI2($x, $ord); + } - $fXAbs = abs($x); - $fResult = exp($fXAbs) / sqrt($f_2_PI * $fXAbs); - if (($ord & 1) && ($x < 0)) { - $fResult = -$fResult; + private static function besselI0(float $x): float + { + $ax = abs($x); + + if ($ax < 3.75) { + $y = $x / 3.75; + $y = $y * $y; + + return 1.0 + $y * (3.5156229 + $y * (3.0899424 + $y * (1.2067492 + + $y * (0.2659732 + $y * (0.360768e-1 + $y * 0.45813e-2))))); } - return $fResult; + $y = 3.75 / $ax; + + return (exp($ax) / sqrt($ax)) * (0.39894228 + $y * (0.1328592e-1 + $y * (0.225319e-2 + $y * (-0.157565e-2 + + $y * (0.916281e-2 + $y * (-0.2057706e-1 + $y * (0.2635537e-1 + + $y * (-0.1647633e-1 + $y * 0.392377e-2)))))))); + } + + private static function besselI1(float $x): float + { + $ax = abs($x); + + if ($ax < 3.75) { + $y = $x / 3.75; + $y = $y * $y; + $ans = $ax * (0.5 + $y * (0.87890594 + $y * (0.51498869 + $y * (0.15084934 + $y * (0.2658733e-1 + + $y * (0.301532e-2 + $y * 0.32411e-3)))))); + + return ($x < 0.0) ? -$ans : $ans; + } + + $y = 3.75 / $ax; + $ans = 0.2282967e-1 + $y * (-0.2895312e-1 + $y * (0.1787654e-1 - $y * 0.420059e-2)); + $ans = 0.39894228 + $y * (-0.3988024e-1 + $y * (-0.362018e-2 + $y * (0.163801e-2 + + $y * (-0.1031555e-1 + $y * $ans)))); + $ans *= exp($ax) / sqrt($ax); + + return ($x < 0.0) ? -$ans : $ans; + } + + private static function besselI2(float $x, int $ord): float + { + if ($x === 0.0) { + return 0.0; + } + + $tox = 2.0 / abs($x); + $bip = 0; + $ans = 0.0; + $bi = 1.0; + + for ($j = 2 * ($ord + (int) sqrt(40.0 * $ord)); $j > 0; --$j) { + $bim = $bip + $j * $tox * $bi; + $bip = $bi; + $bi = $bim; + + if (abs($bi) > 1.0e+12) { + $ans *= 1.0e-12; + $bi *= 1.0e-12; + $bip *= 1.0e-12; + } + + if ($j === $ord) { + $ans = $bip; + } + } + + $ans *= self::besselI0($x) / $bi; + + return ($x < 0.0 && (($ord % 2) === 1)) ? -$ans : $ans; } } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php index 62dd343a..5e8bfbf5 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class BesselJ { @@ -15,9 +14,12 @@ class BesselJ * Excel Function: * BESSELJ(x,ord) * - * @param float $x The value at which to evaluate the function. + * NOTE: The MS Excel implementation of the BESSELJ function is still not accurate, particularly for higher order + * values with x < -8 and x > 8. This code provides a more accurate calculation + * + * @param mixed (float) $x The value at which to evaluate the function. * If x is nonnumeric, BESSELJ returns the #VALUE! error value. - * @param int $ord The order of the Bessel function. If n is not an integer, it is truncated. + * @param mixed (int) $ord The order of the Bessel function. If n 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. * @@ -34,7 +36,7 @@ class BesselJ return Functions::NAN(); } - $fResult = self::calculate($x, $ord); + $fResult = self::calculate((float) $x, $ord); return (is_nan($fResult)) ? Functions::NAN() : $fResult; } @@ -44,28 +46,123 @@ class BesselJ private static function calculate(float $x, int $ord): float { - if (abs($x) <= 30) { - $fResult = $fTerm = ($x / 2) ** $ord / MathTrig::FACT($ord); - $ordK = 1; - $fSqrX = ($x * $x) / -4; - do { - $fTerm *= $fSqrX; - $fTerm /= ($ordK * ($ordK + $ord)); - $fResult += $fTerm; - } while ((abs($fTerm) > 1e-12) && (++$ordK < 100)); - - return $fResult; + // special cases + switch ($ord) { + case 0: + return self::besselJ0($x); + case 1: + return self::besselJ1($x); } - $f_PI_DIV_2 = M_PI / 2; - $f_PI_DIV_4 = M_PI / 4; + return self::besselJ2($x, $ord); + } - $fXAbs = abs($x); - $fResult = sqrt(Functions::M_2DIVPI / $fXAbs) * cos($fXAbs - $ord * $f_PI_DIV_2 - $f_PI_DIV_4); - if (($ord & 1) && ($x < 0)) { - $fResult = -$fResult; + private static function besselJ0(float $x): float + { + $ax = abs($x); + + if ($ax < 8.0) { + $y = $x * $x; + $ans1 = 57568490574.0 + $y * (-13362590354.0 + $y * (651619640.7 + $y * (-11214424.18 + $y * + (77392.33017 + $y * (-184.9052456))))); + $ans2 = 57568490411.0 + $y * (1029532985.0 + $y * (9494680.718 + $y * (59272.64853 + $y * + (267.8532712 + $y * 1.0)))); + + return $ans1 / $ans2; } - return $fResult; + $z = 8.0 / $ax; + $y = $z * $z; + $xx = $ax - 0.785398164; + $ans1 = 1.0 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6))); + $ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * + (0.7621095161e-6 - $y * 0.934935152e-7))); + + return sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2); + } + + private static function besselJ1(float $x): float + { + $ax = abs($x); + + if ($ax < 8.0) { + $y = $x * $x; + $ans1 = $x * (72362614232.0 + $y * (-7895059235.0 + $y * (242396853.1 + $y * + (-2972611.439 + $y * (15704.48260 + $y * (-30.16036606)))))); + $ans2 = 144725228442.0 + $y * (2300535178.0 + $y * (18583304.74 + $y * (99447.43394 + $y * + (376.9991397 + $y * 1.0)))); + + return $ans1 / $ans2; + } + + $z = 8.0 / $ax; + $y = $z * $z; + $xx = $ax - 2.356194491; + + $ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6)))); + $ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y * + (-0.88228987e-6 + $y * 0.105787412e-6))); + $ans = sqrt(0.636619772 / $ax) * (cos($xx) * $ans1 - $z * sin($xx) * $ans2); + + return ($x < 0.0) ? -$ans : $ans; + } + + private static function besselJ2(float $x, int $ord): float + { + $ax = abs($x); + if ($ax === 0.0) { + return 0.0; + } + + if ($ax > $ord) { + return self::besselj2a($ax, $ord, $x); + } + + return self::besselj2b($ax, $ord, $x); + } + + private static function besselj2a(float $ax, int $ord, float $x) + { + $tox = 2.0 / $ax; + $bjm = self::besselJ0($ax); + $bj = self::besselJ1($ax); + for ($j = 1; $j < $ord; ++$j) { + $bjp = $j * $tox * $bj - $bjm; + $bjm = $bj; + $bj = $bjp; + } + $ans = $bj; + + return ($x < 0.0 && ($ord % 2) == 1) ? -$ans : $ans; + } + + private static function besselj2b(float $ax, int $ord, float $x) + { + $tox = 2.0 / $ax; + $jsum = false; + $bjp = $ans = $sum = 0.0; + $bj = 1.0; + for ($j = 2 * ($ord + (int) sqrt(40.0 * $ord)); $j > 0; --$j) { + $bjm = $j * $tox * $bj - $bjp; + $bjp = $bj; + $bj = $bjm; + if (abs($bj) > 1.0e+10) { + $bj *= 1.0e-10; + $bjp *= 1.0e-10; + $ans *= 1.0e-10; + $sum *= 1.0e-10; + } + if ($jsum === true) { + $sum += $bj; + } + $jsum = !$jsum; + if ($j === $ord) { + $ans = $bjp; + } + } + $sum = 2.0 * $sum - $bj; + $ans /= $sum; + + return ($x < 0.0 && ($ord % 2) === 1) ? -$ans : $ans; } } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php index f64f38b0..ff32b78a 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php @@ -15,9 +15,9 @@ class BesselK * Excel Function: * BESSELK(x,ord) * - * @param float $x The value at which to evaluate the function. + * @param mixed (float) $x The value at which to evaluate the function. * If x is nonnumeric, BESSELK returns the #VALUE! error value. - * @param int $ord The order of the Bessel function. If n is not an integer, it is truncated. + * @param mixed (int) $ord The order of the Bessel function. If n is not an integer, it is truncated. * If $ord is nonnumeric, BESSELK returns the #VALUE! error value. * If $ord < 0, BESSELK returns the #NUM! error value. * @@ -29,22 +29,13 @@ class BesselK $ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord); if ((is_numeric($x)) && (is_numeric($ord))) { - if (($ord < 0) || ($x == 0.0)) { + $ord = (int) floor($ord); + $x = (float) $x; + if (($ord < 0) || ($x <= 0.0)) { return Functions::NAN(); } - switch (floor($ord)) { - case 0: - $fBk = self::besselK0($x); - - break; - case 1: - $fBk = self::besselK1($x); - - break; - default: - $fBk = self::besselK2($x, $ord); - } + $fBk = self::calculate($x, $ord); return (is_nan($fBk)) ? Functions::NAN() : $fBk; } @@ -52,38 +43,51 @@ class BesselK return Functions::VALUE(); } - private static function besselK0(float $fNum): float + private static function calculate($x, $ord): float { - if ($fNum <= 2) { - $fNum2 = $fNum * 0.5; + // special cases + switch (floor($ord)) { + case 0: + return self::besselK0($x); + case 1: + return self::besselK1($x); + } + + return self::besselK2($x, $ord); + } + + private static function besselK0(float $x): float + { + if ($x <= 2) { + $fNum2 = $x * 0.5; $y = ($fNum2 * $fNum2); - return -log($fNum2) * BesselI::BESSELI($fNum, 0) + + return -log($fNum2) * BesselI::BESSELI($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)))))); } - $y = 2 / $fNum; + $y = 2 / $x; - return exp(-$fNum) / sqrt($fNum) * + return exp(-$x) / sqrt($x) * (1.25331414 + $y * (-0.7832358e-1 + $y * (0.2189568e-1 + $y * (-0.1062446e-1 + $y * (0.587872e-2 + $y * (-0.251540e-2 + $y * 0.53208e-3)))))); } - private static function besselK1(float $fNum): float + private static function besselK1(float $x): float { - if ($fNum <= 2) { - $fNum2 = $fNum * 0.5; + if ($x <= 2) { + $fNum2 = $x * 0.5; $y = ($fNum2 * $fNum2); - return log($fNum2) * BesselI::BESSELI($fNum, 1) + + return log($fNum2) * BesselI::BESSELI($x, 1) + (1 + $y * (0.15443144 + $y * (-0.67278579 + $y * (-0.18156897 + $y * (-0.1919402e-1 + $y * - (-0.110404e-2 + $y * (-0.4686e-4))))))) / $fNum; + (-0.110404e-2 + $y * (-0.4686e-4))))))) / $x; } - $y = 2 / $fNum; + $y = 2 / $x; - return exp(-$fNum) / sqrt($fNum) * + return exp(-$x) / sqrt($x) * (1.25331414 + $y * (0.23498619 + $y * (-0.3655620e-1 + $y * (0.1504268e-1 + $y * (-0.780353e-2 + $y * (0.325614e-2 + $y * (-0.68245e-3))))))); } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php index 3bda914c..09694381 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php @@ -14,11 +14,11 @@ class BesselY * Excel Function: * BESSELY(x,ord) * - * @param float $x The value at which to evaluate the function. - * If x is nonnumeric, BESSELY returns the #VALUE! error value. - * @param int $ord The order of the Bessel function. If n 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. + * @param mixed (float) $x The value at which to evaluate the function. + * If x is nonnumeric, BESSELY returns the #VALUE! error value. + * @param mixed (int) $ord The order of the Bessel function. If n 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. * * @return float|string Result, or a string containing an error */ @@ -28,22 +28,13 @@ class BesselY $ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord); if ((is_numeric($x)) && (is_numeric($ord))) { - if (($ord < 0) || ($x == 0.0)) { + $ord = (int) floor($ord); + $x = (float) $x; + if (($ord < 0) || ($x <= 0.0)) { return Functions::NAN(); } - switch (floor($ord)) { - case 0: - $fBy = self::besselY0($x); - - break; - case 1: - $fBy = self::besselY1($x); - - break; - default: - $fBy = self::besselY2($x, $ord); - } + $fBy = self::calculate($x, $ord); return (is_nan($fBy)) ? Functions::NAN() : $fBy; } @@ -51,46 +42,66 @@ class BesselY return Functions::VALUE(); } - private static function besselY0(float $fNum): float + private static function calculate($x, $ord): float { - if ($fNum < 8.0) { - $y = ($fNum * $fNum); - $f1 = -2957821389.0 + $y * (7062834065.0 + $y * (-512359803.6 + $y * (10879881.29 + $y * + // special cases + switch (floor($ord)) { + case 0: + return self::besselY0($x); + case 1: + return self::besselY1($x); + } + + return self::besselY2($x, $ord); + } + + private static function besselY0(float $x): float + { + if ($x < 8.0) { + $y = ($x * $x); + $ans1 = -2957821389.0 + $y * (7062834065.0 + $y * (-512359803.6 + $y * (10879881.29 + $y * (-86327.92757 + $y * 228.4622733)))); - $f2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y * + $ans2 = 40076544269.0 + $y * (745249964.8 + $y * (7189466.438 + $y * (47447.26470 + $y * (226.1030244 + $y)))); - return $f1 / $f2 + 0.636619772 * BesselJ::BESSELJ($fNum, 0) * log($fNum); + return $ans1 / $ans2 + 0.636619772 * BesselJ::BESSELJ($x, 0) * log($x); } - $z = 8.0 / $fNum; + $z = 8.0 / $x; $y = ($z * $z); - $xx = $fNum - 0.785398164; - $f1 = 1 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6))); - $f2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * (0.7621095161e-6 + $y * + $xx = $x - 0.785398164; + $ans1 = 1 + $y * (-0.1098628627e-2 + $y * (0.2734510407e-4 + $y * (-0.2073370639e-5 + $y * 0.2093887211e-6))); + $ans2 = -0.1562499995e-1 + $y * (0.1430488765e-3 + $y * (-0.6911147651e-5 + $y * (0.7621095161e-6 + $y * (-0.934945152e-7)))); - return sqrt(0.636619772 / $fNum) * (sin($xx) * $f1 + $z * cos($xx) * $f2); + return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2); } - private static function besselY1(float $fNum): float + private static function besselY1(float $x): float { - if ($fNum < 8.0) { - $y = ($fNum * $fNum); - $f1 = $fNum * (-0.4900604943e13 + $y * (0.1275274390e13 + $y * (-0.5153438139e11 + $y * + if ($x < 8.0) { + $y = ($x * $x); + $ans1 = $x * (-0.4900604943e13 + $y * (0.1275274390e13 + $y * (-0.5153438139e11 + $y * (0.7349264551e9 + $y * (-0.4237922726e7 + $y * 0.8511937935e4))))); - $f2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y * + $ans2 = 0.2499580570e14 + $y * (0.4244419664e12 + $y * (0.3733650367e10 + $y * (0.2245904002e8 + $y * (0.1020426050e6 + $y * (0.3549632885e3 + $y))))); - return $f1 / $f2 + 0.636619772 * (BesselJ::BESSELJ($fNum, 1) * log($fNum) - 1 / $fNum); + return ($ans1 / $ans2) + 0.636619772 * (BesselJ::BESSELJ($x, 1) * log($x) - 1 / $x); } - return sqrt(0.636619772 / $fNum) * sin($fNum - 2.356194491); + $z = 8.0 / $x; + $y = $z * $z; + $xx = $x - 2.356194491; + $ans1 = 1.0 + $y * (0.183105e-2 + $y * (-0.3516396496e-4 + $y * (0.2457520174e-5 + $y * (-0.240337019e-6)))); + $ans2 = 0.04687499995 + $y * (-0.2002690873e-3 + $y * (0.8449199096e-5 + $y * + (-0.88228987e-6 + $y * 0.105787412e-6))); + + return sqrt(0.636619772 / $x) * (sin($xx) * $ans1 + $z * cos($xx) * $ans2); } - private static function besselY2(float $x, int $ord) + private static function besselY2(float $x, int $ord): float { - $fTox = 2 / $x; + $fTox = 2.0 / $x; $fBym = self::besselY0($x); $fBy = self::besselY1($x); for ($n = 1; $n < $ord; ++$n) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselITest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselITest.php index 8fff98af..5b6ba045 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselITest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselITest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase; class BesselITest extends TestCase { - const BESSEL_PRECISION = 1E-8; + const BESSEL_PRECISION = 1E-9; protected function setUp(): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselKTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselKTest.php index 27123a26..23ad3539 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselKTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselKTest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase; class BesselKTest extends TestCase { - const BESSEL_PRECISION = 1E-8; + const BESSEL_PRECISION = 1E-12; protected function setUp(): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselYTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselYTest.php index ab55f0ac..4422ad50 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselYTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Engineering/BesselYTest.php @@ -8,7 +8,7 @@ use PHPUnit\Framework\TestCase; class BesselYTest extends TestCase { - const BESSEL_PRECISION = 1E-8; + const BESSEL_PRECISION = 1E-12; protected function setUp(): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php index e7907384..8c637f9a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ReptTest.php @@ -1,10 +1,10 @@ Date: Wed, 24 Mar 2021 20:05:32 +0100 Subject: [PATCH 60/89] Csv reader refactor infer delimiter (#1948) * Refactor delimiter inference for CSV file reading into a separate class --- src/PhpSpreadsheet/Reader/Csv.php | 103 +------------- src/PhpSpreadsheet/Reader/Csv/Delimiter.php | 144 ++++++++++++++++++++ 2 files changed, 150 insertions(+), 97 deletions(-) create mode 100644 src/PhpSpreadsheet/Reader/Csv/Delimiter.php diff --git a/src/PhpSpreadsheet/Reader/Csv.php b/src/PhpSpreadsheet/Reader/Csv.php index 92b0f6ac..dc746735 100644 --- a/src/PhpSpreadsheet/Reader/Csv.php +++ b/src/PhpSpreadsheet/Reader/Csv.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; use InvalidArgumentException; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Reader\Csv\Delimiter; use PhpOffice\PhpSpreadsheet\Shared\StringHelper; use PhpOffice\PhpSpreadsheet\Spreadsheet; @@ -138,118 +139,26 @@ class Csv extends BaseReader return; } - $potentialDelimiters = [',', ';', "\t", '|', ':', ' ', '~']; - $counts = []; - foreach ($potentialDelimiters as $delimiter) { - $counts[$delimiter] = []; - } - - // Count how many times each of the potential delimiters appears in each line - $numberLines = 0; - while (($line = $this->getNextLine()) !== false && (++$numberLines < 1000)) { - $countLine = []; - for ($i = strlen($line) - 1; $i >= 0; --$i) { - $char = $line[$i]; - if (isset($counts[$char])) { - if (!isset($countLine[$char])) { - $countLine[$char] = 0; - } - ++$countLine[$char]; - } - } - foreach ($potentialDelimiters as $delimiter) { - $counts[$delimiter][] = $countLine[$delimiter] - ?? 0; - } - } + $inferenceEngine = new Delimiter($this->fileHandle, $this->escapeCharacter, $this->enclosure); // If number of lines is 0, nothing to infer : fall back to the default - if ($numberLines === 0) { - $this->delimiter = reset($potentialDelimiters); + if ($inferenceEngine->linesCounted() === 0) { + $this->delimiter = $inferenceEngine->getDefaultDelimiter(); $this->skipBOM(); return; } - // Calculate the mean square deviations for each delimiter (ignoring delimiters that haven't been found consistently) - $meanSquareDeviations = []; - $middleIdx = floor(($numberLines - 1) / 2); - - foreach ($potentialDelimiters as $delimiter) { - $series = $counts[$delimiter]; - sort($series); - - $median = ($numberLines % 2) - ? $series[$middleIdx] - : ($series[$middleIdx] + $series[$middleIdx + 1]) / 2; - - if ($median === 0) { - continue; - } - - $meanSquareDeviations[$delimiter] = array_reduce( - $series, - function ($sum, $value) use ($median) { - return $sum + ($value - $median) ** 2; - } - ) / count($series); - } - - // ... and pick the delimiter with the smallest mean square deviation (in case of ties, the order in potentialDelimiters is respected) - $min = INF; - foreach ($potentialDelimiters as $delimiter) { - if (!isset($meanSquareDeviations[$delimiter])) { - continue; - } - - if ($meanSquareDeviations[$delimiter] < $min) { - $min = $meanSquareDeviations[$delimiter]; - $this->delimiter = $delimiter; - } - } + $this->delimiter = $inferenceEngine->infer(); // If no delimiter could be detected, fall back to the default if ($this->delimiter === null) { - $this->delimiter = reset($potentialDelimiters); + $this->delimiter = $inferenceEngine->getDefaultDelimiter(); } $this->skipBOM(); } - /** - * Get the next full line from the file. - * - * @return false|string - */ - private function getNextLine() - { - $line = ''; - $enclosure = ($this->escapeCharacter === '' ? '' - : ('(?escapeCharacter, '/') . ')')) - . preg_quote($this->enclosure, '/'); - - do { - // Get the next line in the file - $newLine = fgets($this->fileHandle); - - // Return false if there is no next line - if ($newLine === false) { - return false; - } - - // Add the new line to the line passed in - $line = $line . $newLine; - - // Drop everything that is enclosed to avoid counting false positives in enclosures - $line = 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); - - return $line; - } - /** * Return worksheet info (Name, Last Column Letter, Last Column Index, Total Rows, Total Columns). * diff --git a/src/PhpSpreadsheet/Reader/Csv/Delimiter.php b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php new file mode 100644 index 00000000..eb62c9ac --- /dev/null +++ b/src/PhpSpreadsheet/Reader/Csv/Delimiter.php @@ -0,0 +1,144 @@ +fileHandle = $fileHandle; + $this->escapeCharacter = $escapeCharacter; + $this->enclosure = $enclosure; + + $this->countPotentialDelimiters(); + } + + public function getDefaultDelimiter(): string + { + return self::POTENTIAL_DELIMETERS[0]; + } + + public function linesCounted(): int + { + return $this->numberLines; + } + + protected function countPotentialDelimiters(): void + { + $this->counts = array_fill_keys(self::POTENTIAL_DELIMETERS, []); + $delimiterKeys = array_flip(self::POTENTIAL_DELIMETERS); + + // Count how many times each of the potential delimiters appears in each line + $this->numberLines = 0; + while (($line = $this->getNextLine()) !== false && (++$this->numberLines < 1000)) { + $this->countDelimiterValues($line, $delimiterKeys); + } + } + + protected function countDelimiterValues(string $line, array $delimiterKeys): void + { + $splitString = str_split($line, 1); + if (!is_array($splitString)) { + return; + } + + $distribution = array_count_values($splitString); + $countLine = array_intersect_key($distribution, $delimiterKeys); + + foreach (self::POTENTIAL_DELIMETERS as $delimiter) { + $this->counts[$delimiter][] = $countLine[$delimiter] ?? 0; + } + } + + public function infer(): ?string + { + // Calculate the mean square deviations for each delimiter + // (ignoring delimiters that haven't been found consistently) + $meanSquareDeviations = []; + $middleIdx = floor(($this->numberLines - 1) / 2); + + foreach (self::POTENTIAL_DELIMETERS as $delimiter) { + $series = $this->counts[$delimiter]; + sort($series); + + $median = ($this->numberLines % 2) + ? $series[$middleIdx] + : ($series[$middleIdx] + $series[$middleIdx + 1]) / 2; + + if ($median === 0) { + continue; + } + + $meanSquareDeviations[$delimiter] = array_reduce( + $series, + function ($sum, $value) use ($median) { + return $sum + ($value - $median) ** 2; + } + ) / count($series); + } + + // ... and pick the delimiter with the smallest mean square deviation + // (in case of ties, the order in potentialDelimiters is respected) + $min = INF; + foreach (self::POTENTIAL_DELIMETERS as $delimiter) { + if (!isset($meanSquareDeviations[$delimiter])) { + continue; + } + + if ($meanSquareDeviations[$delimiter] < $min) { + $min = $meanSquareDeviations[$delimiter]; + $this->delimiter = $delimiter; + } + } + + return $this->delimiter; + } + + /** + * Get the next full line from the file. + * + * @return false|string + */ + public function getNextLine() + { + $line = ''; + $enclosure = ($this->escapeCharacter === '' ? '' + : ('(?escapeCharacter, '/') . ')')) + . preg_quote($this->enclosure, '/'); + + do { + // Get the next line in the file + $newLine = fgets($this->fileHandle); + + // Return false if there is no next line + if ($newLine === false) { + return false; + } + + // Add the new line to the line passed in + $line = $line . $newLine; + + // Drop everything that is enclosed to avoid counting false positives in enclosures + $line = 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); + + return $line; + } +} From f51c19c1255972047f33451a510905dab5862677 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Thu, 25 Mar 2021 20:54:55 +0100 Subject: [PATCH 61/89] First steps toward refactoring Excel's Statistical Distributions (#1949) * First steps toward refactoring Statistical Distributions into smaller classes: BETA() and GAMMA() (and related functions) to start with... they all need a lot of tidying up, and more testing; but it's a start * Add basic datatype validations to Beta and Gamma Excel function implementations * Switch to using a trait with the validation methods to provide easier sharing between distribution classes * Additional unit tests for Beta and Gamma functions, including unhappy path for validations * Extract ChiSquared functions * Additional argument validation checks with unit tests for Chi Squared functions * Extract Fisher * Move MEDIAN() and MODE() to the Averages class * Extract filters for Median and Mode for common usage --- .../Calculation/Calculation.php | 44 +- .../Calculation/Database/DAverage.php | 2 +- .../Calculation/Financial/Amortization.php | 16 +- .../Calculation/Financial/Coupons.php | 14 +- .../Calculation/Financial/Depreciation.php | 20 +- .../Calculation/Financial/Dollar.php | 8 +- .../Calculation/Financial/InterestRate.php | 8 +- .../Financial/Securities/Price.php | 22 +- .../Calculation/Financial/TreasuryBill.php | 6 +- .../Calculation/LookupRef/Address.php | 6 +- .../Calculation/LookupRef/Indirect.php | 2 +- src/PhpSpreadsheet/Calculation/MathTrig.php | 2 +- .../Calculation/Statistical.php | 838 ++---------------- .../Calculation/Statistical/Averages.php | 130 ++- .../Calculation/Statistical/Confidence.php | 6 +- .../Distributions/BaseValidations.php | 36 + .../Statistical/Distributions/Beta.php | 263 ++++++ .../Statistical/Distributions/ChiSquared.php | 127 +++ .../Statistical/Distributions/Fisher.php | 63 ++ .../Statistical/Distributions/Gamma.php | 129 +++ .../Statistical/Distributions/GammaBase.php | 377 ++++++++ .../Calculation/Statistical/Permutations.php | 4 +- .../Statistical/StandardDeviations.php | 8 +- .../Calculation/Statistical/Trends.php | 14 +- .../Calculation/TextData/CaseConvert.php | 6 +- .../Calculation/TextData/CharacterConvert.php | 4 +- .../Calculation/TextData/Extract.php | 14 +- .../Calculation/TextData/Format.php | 14 +- .../Calculation/TextData/Replace.php | 16 +- .../Calculation/TextData/Search.php | 12 +- .../Calculation/TextData/Text.php | 6 +- .../Calculation/TextData/Trim.php | 4 +- .../data/Calculation/Statistical/BETADIST.php | 44 + .../data/Calculation/Statistical/BETAINV.php | 44 + .../data/Calculation/Statistical/CHIDIST.php | 12 +- tests/data/Calculation/Statistical/CHIINV.php | 18 +- tests/data/Calculation/Statistical/FISHER.php | 10 +- .../Calculation/Statistical/FISHERINV.php | 4 + tests/data/Calculation/Statistical/GAMMA.php | 4 +- .../Calculation/Statistical/GAMMADIST.php | 32 + .../data/Calculation/Statistical/GAMMAINV.php | 30 +- .../data/Calculation/Statistical/GAMMALN.php | 6 +- tests/data/Calculation/TextData/FIND.php | 5 + tests/data/Calculation/TextData/SEARCH.php | 5 + 44 files changed, 1548 insertions(+), 887 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index c5dbaa53..80a6fbcc 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -328,17 +328,17 @@ class Calculation ], 'AVEDEV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical\Averages::class, 'AVEDEV'], + 'functionCall' => [Statistical\Averages::class, 'averageDeviations'], 'argumentCount' => '1+', ], 'AVERAGE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical\Averages::class, 'AVERAGE'], + 'functionCall' => [Statistical\Averages::class, 'average'], 'argumentCount' => '1+', ], 'AVERAGEA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical\Averages::class, 'AVERAGEA'], + 'functionCall' => [Statistical\Averages::class, 'averageA'], 'argumentCount' => '1+', ], 'AVERAGEIF' => [ @@ -383,7 +383,7 @@ class Calculation ], 'BETADIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'BETADIST'], + 'functionCall' => [Statistical\Distributions\Beta::class, 'distribution'], 'argumentCount' => '3-5', ], 'BETA.DIST' => [ @@ -393,12 +393,12 @@ class Calculation ], 'BETAINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'BETAINV'], + 'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'], 'argumentCount' => '3-5', ], 'BETA.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'BETAINV'], + 'functionCall' => [Statistical\Distributions\Beta::class, 'inverse'], 'argumentCount' => '3-5', ], 'BIN2DEC' => [ @@ -488,7 +488,7 @@ class Calculation ], 'CHIDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CHIDIST'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distribution'], 'argumentCount' => '2', ], 'CHISQ.DIST' => [ @@ -498,12 +498,12 @@ class Calculation ], 'CHISQ.DIST.RT' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CHIDIST'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distribution'], 'argumentCount' => '2', ], 'CHIINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CHIINV'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverse'], 'argumentCount' => '2', ], 'CHISQ.INV' => [ @@ -513,7 +513,7 @@ class Calculation ], 'CHISQ.INV.RT' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CHIINV'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverse'], 'argumentCount' => '2', ], 'CHITEST' => [ @@ -1055,12 +1055,12 @@ class Calculation ], 'FISHER' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'FISHER'], + 'functionCall' => [Statistical\Distributions\Fisher::class, 'distribution'], 'argumentCount' => '1', ], 'FISHERINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'FISHERINV'], + 'functionCall' => [Statistical\Distributions\Fisher::class, 'inverse'], 'argumentCount' => '1', ], 'FIXED' => [ @@ -1147,37 +1147,37 @@ class Calculation ], 'GAMMA' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GAMMAFunction'], + 'functionCall' => [Statistical\Distributions\Gamma::class, 'gamma'], 'argumentCount' => '1', ], 'GAMMADIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GAMMADIST'], + 'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'], 'argumentCount' => '4', ], 'GAMMA.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GAMMADIST'], + 'functionCall' => [Statistical\Distributions\Gamma::class, 'distribution'], 'argumentCount' => '4', ], 'GAMMAINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GAMMAINV'], + 'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'], 'argumentCount' => '3', ], 'GAMMA.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GAMMAINV'], + 'functionCall' => [Statistical\Distributions\Gamma::class, 'inverse'], 'argumentCount' => '3', ], 'GAMMALN' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GAMMALN'], + 'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'], 'argumentCount' => '1', ], 'GAMMALN.PRECISE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'GAMMALN'], + 'functionCall' => [Statistical\Distributions\Gamma::class, 'ln'], 'argumentCount' => '1', ], 'GAUSS' => [ @@ -1646,7 +1646,7 @@ class Calculation ], 'MEDIAN' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MEDIAN'], + 'functionCall' => [Statistical\Averages::class, 'median'], 'argumentCount' => '1+', ], 'MEDIANIF' => [ @@ -1706,7 +1706,7 @@ class Calculation ], 'MODE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MODE'], + 'functionCall' => [Statistical\Averages::class, 'mode'], 'argumentCount' => '1+', ], 'MODE.MULT' => [ @@ -1716,7 +1716,7 @@ class Calculation ], 'MODE.SNGL' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'MODE'], + 'functionCall' => [Statistical\Averages::class, 'mode'], 'argumentCount' => '1+', ], 'MONTH' => [ diff --git a/src/PhpSpreadsheet/Calculation/Database/DAverage.php b/src/PhpSpreadsheet/Calculation/Database/DAverage.php index 738cb78e..e30842dc 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DAverage.php +++ b/src/PhpSpreadsheet/Calculation/Database/DAverage.php @@ -38,7 +38,7 @@ class DAverage extends DatabaseAbstract return null; } - return Averages::AVERAGE( + return Averages::average( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php index 76be7e12..7bb7fb40 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php @@ -22,13 +22,13 @@ class Amortization * Excel Function: * AMORDEGRC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) * - * @param float $cost The cost of the asset + * @param mixed (float) $cost The cost of the asset * @param mixed $purchased Date of the purchase of the asset * @param mixed $firstPeriod Date of the end of the first period * @param mixed $salvage The salvage value at the end of the life of the asset - * @param float $period The period - * @param float $rate Rate of depreciation - * @param int $basis The type of day count to use. + * @param mixed (float) $period The period + * @param mixed (float) $rate Rate of depreciation + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -88,13 +88,13 @@ class Amortization * Excel Function: * AMORLINC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) * - * @param float $cost The cost of the asset + * @param mixed (float) $cost The cost of the asset * @param mixed $purchased Date of the purchase of the asset * @param mixed $firstPeriod Date of the end of the first period * @param mixed $salvage The salvage value at the end of the life of the asset - * @param float $period The period - * @param float $rate Rate of depreciation - * @param int $basis The type of day count to use. + * @param mixed (float) $period The period + * @param mixed (float) $rate Rate of depreciation + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index 835ef633..d0efd689 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -29,12 +29,12 @@ class Coupons * date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. - * @param int $frequency the number of coupon payments per year. + * @param mixed (int) $frequency the number of coupon payments per year. * Valid frequency values are: * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -88,7 +88,7 @@ class Coupons * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -153,7 +153,7 @@ class Coupons * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -211,7 +211,7 @@ class Coupons * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -260,7 +260,7 @@ class Coupons * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -309,7 +309,7 @@ class Coupons * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 diff --git a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php index 9236b4d4..173e29bb 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php @@ -20,14 +20,14 @@ class Depreciation * Excel Function: * DB(cost,salvage,life,period[,month]) * - * @param float $cost Initial cost of the asset - * @param float $salvage Value at the end of the depreciation. + * @param mixed (float) $cost Initial cost of the asset + * @param mixed (float) $salvage Value at the end of the depreciation. * (Sometimes called the salvage value of the asset) - * @param int $life Number of periods over which the asset is depreciated. + * @param mixed (int) $life Number of periods over which the asset is depreciated. * (Sometimes called the useful life of the asset) - * @param int $period The period for which you want to calculate the + * @param mixed (int) $period The period for which you want to calculate the * depreciation. Period must use the same units as life. - * @param int $month Number of months in the first year. If month is omitted, + * @param mixed (int) $month Number of months in the first year. If month is omitted, * it defaults to 12. * * @return float|string @@ -85,14 +85,14 @@ class Depreciation * Excel Function: * DDB(cost,salvage,life,period[,factor]) * - * @param float $cost Initial cost of the asset - * @param float $salvage Value at the end of the depreciation. + * @param mixed (float) $cost Initial cost of the asset + * @param mixed (float) $salvage Value at the end of the depreciation. * (Sometimes called the salvage value of the asset) - * @param int $life Number of periods over which the asset is depreciated. + * @param mixed (int) $life Number of periods over which the asset is depreciated. * (Sometimes called the useful life of the asset) - * @param int $period The period for which you want to calculate the + * @param mixed (int) $period The period for which you want to calculate the * depreciation. Period must use the same units as life. - * @param float $factor The rate at which the balance declines. + * @param mixed (float) $factor The rate at which the balance declines. * If factor is omitted, it is assumed to be 2 (the * double-declining balance method). * diff --git a/src/PhpSpreadsheet/Calculation/Financial/Dollar.php b/src/PhpSpreadsheet/Calculation/Financial/Dollar.php index e85b00c6..36326a60 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Dollar.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Dollar.php @@ -16,8 +16,8 @@ class Dollar * Excel Function: * DOLLARDE(fractional_dollar,fraction) * - * @param float $fractionalDollar Fractional Dollar - * @param int $fraction Fraction + * @param mixed (float) $fractionalDollar Fractional Dollar + * @param mixed (int) $fraction Fraction * * @return float|string */ @@ -52,8 +52,8 @@ class Dollar * Excel Function: * DOLLARFR(decimal_dollar,fraction) * - * @param float $decimalDollar Decimal Dollar - * @param int $fraction Fraction + * @param mixed (float) $decimalDollar Decimal Dollar + * @param mixed (int) $fraction Fraction * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php b/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php index be7e6fd7..04b43e32 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php +++ b/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php @@ -15,8 +15,8 @@ class InterestRate * Excel Function: * EFFECT(nominal_rate,npery) * - * @param float $nominalRate Nominal interest rate - * @param int $periodsPerYear Number of compounding payments per year + * @param mixed (float) $nominalRate Nominal interest rate + * @param mixed (int) $periodsPerYear Number of compounding payments per year * * @return float|string */ @@ -43,8 +43,8 @@ class InterestRate * * Returns the nominal interest rate given the effective rate and the number of compounding payments per year. * - * @param float $effectiveRate Effective interest rate - * @param int $periodsPerYear Number of compounding payments per year + * @param mixed (float) $effectiveRate Effective interest rate + * @param mixed (int) $periodsPerYear Number of compounding payments per year * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php index a5f0fb46..14be7f84 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php @@ -20,14 +20,14 @@ class Price extends BaseValidations * is traded to the buyer. * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. - * @param float $rate the security's annual coupon rate - * @param float $yield the security's annual yield - * @param float $redemption The number of coupon payments per year. + * @param mixed (float) $rate the security's annual coupon rate + * @param mixed (float) $yield the security's annual yield + * @param mixed (float) $redemption The number of coupon payments per year. * For annual payments, frequency = 1; * for semiannual, frequency = 2; * for quarterly, frequency = 4. - * @param int $frequency - * @param int $basis The type of day count to use. + * @param mixed (int) $frequency + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -87,9 +87,9 @@ class Price extends BaseValidations * is traded to the buyer. * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. - * @param float $discount The security's discount rate - * @param float $redemption The security's redemption value per $100 face value - * @param int $basis The type of day count to use. + * @param mixed (float) $discount The security's discount rate + * @param mixed (float) $redemption The security's redemption value per $100 face value + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -137,9 +137,9 @@ class Price extends BaseValidations * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. * @param mixed $issue The security's issue date - * @param float $rate The security's interest rate at date of issue - * @param float $yield The security's annual yield - * @param int $basis The type of day count to use. + * @param mixed (float) $rate The security's interest rate at date of issue + * @param mixed (float) $yield The security's annual yield + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 diff --git a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php index 8f8fa530..3177124a 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php +++ b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php @@ -17,7 +17,7 @@ class TreasuryBill * when the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. - * @param int $discount The Treasury bill's discount rate + * @param mixed (int) $discount The Treasury bill's discount rate * * @return float|string Result, or a string containing an error */ @@ -65,7 +65,7 @@ class TreasuryBill * when the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. - * @param int $discount The Treasury bill's discount rate + * @param mixed (int) $discount The Treasury bill's discount rate * * @return float|string Result, or a string containing an error */ @@ -117,7 +117,7 @@ class TreasuryBill * the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. - * @param int $price The Treasury bill's price per $100 face value + * @param mixed (int) $price The Treasury bill's price per $100 face value * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index 53c9c9d8..daaebea2 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -25,15 +25,15 @@ class Address * * @param mixed $row Row number to use in the cell reference * @param mixed $column Column number to use in the cell reference - * @param int $relativity Flag indicating the type of reference to return + * @param mixed (int) $relativity Flag indicating the type of reference to return * 1 or omitted Absolute * 2 Absolute row; relative column * 3 Relative row; absolute column * 4 Relative - * @param bool $referenceStyle A logical value that specifies the A1 or R1C1 reference style. + * @param mixed (bool) $referenceStyle A logical 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 - * @param string $sheetName Optional Name of worksheet to use + * @param mixed (string) $sheetName Optional Name of worksheet to use * * @return string */ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php index 690b32e4..c34dd965 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Indirect.php @@ -21,7 +21,7 @@ class Indirect * NOTE - INDIRECT() does not yet support the optional a1 parameter introduced in Excel 2010 * * @param null|array|string $cellAddress $cellAddress The cell address of the current cell (containing this formula) - * @param Cell $pCell The current cell (containing this formula) + * @param null|Cell $pCell The current cell (containing this formula) * * @return array|string An array containing a cell or range of cells, or a string on error * diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index f3d8351d..94850906 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -1156,7 +1156,7 @@ class MathTrig $aArgs = self::filterFormulaArgs($cellReference, $aArgs); switch ($subtotal) { case 1: - return Statistical\Averages::AVERAGE($aArgs); + return Statistical\Averages::average($aArgs); case 2: return Statistical\Counts::COUNT($aArgs); case 3: diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 8a9e3fea..c8e084b5 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -12,411 +12,15 @@ use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Permutations; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\StandardDeviations; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Trends; use PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances; -use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend; class Statistical { const LOG_GAMMA_X_MAX_VALUE = 2.55e305; - const XMININ = 2.23e-308; const EPS = 2.22e-16; const MAX_VALUE = 1.2e308; const MAX_ITERATIONS = 256; const SQRT2PI = 2.5066282746310005024157652848110452530069867406099; - /** - * Incomplete beta function. - * - * @author Jaco van Kooten - * @author Paul Meagher - * - * The computation is based on formulas from Numerical Recipes, Chapter 6.4 (W.H. Press et al, 1992). - * - * @param mixed $x require 0<=x<=1 - * @param mixed $p require p>0 - * @param mixed $q require q>0 - * - * @return float 0 if x<0, p<=0, q<=0 or p+q>2.55E305 and 1 if x>1 to avoid errors and over/underflow - */ - private static function incompleteBeta($x, $p, $q) - { - if ($x <= 0.0) { - return 0.0; - } elseif ($x >= 1.0) { - return 1.0; - } elseif (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) { - return 0.0; - } - $beta_gam = exp((0 - self::logBeta($p, $q)) + $p * log($x) + $q * log(1.0 - $x)); - if ($x < ($p + 1.0) / ($p + $q + 2.0)) { - return $beta_gam * self::betaFraction($x, $p, $q) / $p; - } - - return 1.0 - ($beta_gam * self::betaFraction(1 - $x, $q, $p) / $q); - } - - // Function cache for logBeta function - private static $logBetaCacheP = 0.0; - - private static $logBetaCacheQ = 0.0; - - private static $logBetaCacheResult = 0.0; - - /** - * The natural logarithm of the beta function. - * - * @param mixed $p require p>0 - * @param mixed $q require q>0 - * - * @return float 0 if p<=0, q<=0 or p+q>2.55E305 to avoid errors and over/underflow - * - * @author Jaco van Kooten - */ - private static function logBeta($p, $q) - { - if ($p != self::$logBetaCacheP || $q != self::$logBetaCacheQ) { - self::$logBetaCacheP = $p; - self::$logBetaCacheQ = $q; - if (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) { - self::$logBetaCacheResult = 0.0; - } else { - self::$logBetaCacheResult = self::logGamma($p) + self::logGamma($q) - self::logGamma($p + $q); - } - } - - return self::$logBetaCacheResult; - } - - /** - * Evaluates of continued fraction part of incomplete beta function. - * Based on an idea from Numerical Recipes (W.H. Press et al, 1992). - * - * @author Jaco van Kooten - * - * @param mixed $x - * @param mixed $p - * @param mixed $q - * - * @return float - */ - private static function betaFraction($x, $p, $q) - { - $c = 1.0; - $sum_pq = $p + $q; - $p_plus = $p + 1.0; - $p_minus = $p - 1.0; - $h = 1.0 - $sum_pq * $x / $p_plus; - if (abs($h) < self::XMININ) { - $h = self::XMININ; - } - $h = 1.0 / $h; - $frac = $h; - $m = 1; - $delta = 0.0; - while ($m <= self::MAX_ITERATIONS && abs($delta - 1.0) > Functions::PRECISION) { - $m2 = 2 * $m; - // even index for d - $d = $m * ($q - $m) * $x / (($p_minus + $m2) * ($p + $m2)); - $h = 1.0 + $d * $h; - if (abs($h) < self::XMININ) { - $h = self::XMININ; - } - $h = 1.0 / $h; - $c = 1.0 + $d / $c; - if (abs($c) < self::XMININ) { - $c = self::XMININ; - } - $frac *= $h * $c; - // odd index for d - $d = -($p + $m) * ($sum_pq + $m) * $x / (($p + $m2) * ($p_plus + $m2)); - $h = 1.0 + $d * $h; - if (abs($h) < self::XMININ) { - $h = self::XMININ; - } - $h = 1.0 / $h; - $c = 1.0 + $d / $c; - if (abs($c) < self::XMININ) { - $c = self::XMININ; - } - $delta = $h * $c; - $frac *= $delta; - ++$m; - } - - return $frac; - } - - /** - * logGamma function. - * - * @version 1.1 - * - * @author Jaco van Kooten - * - * Original author was Jaco van Kooten. Ported to PHP by Paul Meagher. - * - * The natural logarithm of the gamma function.
- * Based on public domain NETLIB (Fortran) code by W. J. Cody and L. Stoltz
- * Applied Mathematics Division
- * Argonne National Laboratory
- * Argonne, IL 60439
- *

- * References: - *

    - *
  1. W. J. Cody and K. E. Hillstrom, 'Chebyshev Approximations for the Natural - * Logarithm of the Gamma Function,' Math. Comp. 21, 1967, pp. 198-203.
  2. - *
  3. K. E. Hillstrom, ANL/AMD Program ANLC366S, DGAMMA/DLGAMA, May, 1969.
  4. - *
  5. Hart, Et. Al., Computer Approximations, Wiley and sons, New York, 1968.
  6. - *
- *

- *

- * From the original documentation: - *

- *

- * This routine calculates the LOG(GAMMA) function for a positive real argument X. - * Computation is based on an algorithm outlined in references 1 and 2. - * The program uses rational functions that theoretically approximate LOG(GAMMA) - * to at least 18 significant decimal digits. The approximation for X > 12 is from - * reference 3, while approximations for X < 12.0 are similar to those in reference - * 1, but are unpublished. The accuracy achieved depends on the arithmetic system, - * the compiler, the intrinsic functions, and proper selection of the - * machine-dependent constants. - *

- *

- * Error returns:
- * The program returns the value XINF for X .LE. 0.0 or when overflow would occur. - * The computation is believed to be free of underflow and overflow. - *

- * - * @return float MAX_VALUE for x < 0.0 or when overflow would occur, i.e. x > 2.55E305 - */ - - // Function cache for logGamma - private static $logGammaCacheResult = 0.0; - - private static $logGammaCacheX = 0.0; - - private static function logGamma($x) - { - // Log Gamma related constants - static $lg_d1 = -0.5772156649015328605195174; - static $lg_d2 = 0.4227843350984671393993777; - static $lg_d4 = 1.791759469228055000094023; - - static $lg_p1 = [ - 4.945235359296727046734888, - 201.8112620856775083915565, - 2290.838373831346393026739, - 11319.67205903380828685045, - 28557.24635671635335736389, - 38484.96228443793359990269, - 26377.48787624195437963534, - 7225.813979700288197698961, - ]; - static $lg_p2 = [ - 4.974607845568932035012064, - 542.4138599891070494101986, - 15506.93864978364947665077, - 184793.2904445632425417223, - 1088204.76946882876749847, - 3338152.967987029735917223, - 5106661.678927352456275255, - 3074109.054850539556250927, - ]; - static $lg_p4 = [ - 14745.02166059939948905062, - 2426813.369486704502836312, - 121475557.4045093227939592, - 2663432449.630976949898078, - 29403789566.34553899906876, - 170266573776.5398868392998, - 492612579337.743088758812, - 560625185622.3951465078242, - ]; - static $lg_q1 = [ - 67.48212550303777196073036, - 1113.332393857199323513008, - 7738.757056935398733233834, - 27639.87074403340708898585, - 54993.10206226157329794414, - 61611.22180066002127833352, - 36351.27591501940507276287, - 8785.536302431013170870835, - ]; - static $lg_q2 = [ - 183.0328399370592604055942, - 7765.049321445005871323047, - 133190.3827966074194402448, - 1136705.821321969608938755, - 5267964.117437946917577538, - 13467014.54311101692290052, - 17827365.30353274213975932, - 9533095.591844353613395747, - ]; - static $lg_q4 = [ - 2690.530175870899333379843, - 639388.5654300092398984238, - 41355999.30241388052042842, - 1120872109.61614794137657, - 14886137286.78813811542398, - 101680358627.2438228077304, - 341747634550.7377132798597, - 446315818741.9713286462081, - ]; - static $lg_c = [ - -0.001910444077728, - 8.4171387781295e-4, - -5.952379913043012e-4, - 7.93650793500350248e-4, - -0.002777777777777681622553, - 0.08333333333333333331554247, - 0.0057083835261, - ]; - - // Rough estimate of the fourth root of logGamma_xBig - static $lg_frtbig = 2.25e76; - static $pnt68 = 0.6796875; - - if ($x == self::$logGammaCacheX) { - return self::$logGammaCacheResult; - } - $y = $x; - if ($y > 0.0 && $y <= self::LOG_GAMMA_X_MAX_VALUE) { - if ($y <= self::EPS) { - $res = -log($y); - } elseif ($y <= 1.5) { - // --------------------- - // EPS .LT. X .LE. 1.5 - // --------------------- - if ($y < $pnt68) { - $corr = -log($y); - $xm1 = $y; - } else { - $corr = 0.0; - $xm1 = $y - 1.0; - } - if ($y <= 0.5 || $y >= $pnt68) { - $xden = 1.0; - $xnum = 0.0; - for ($i = 0; $i < 8; ++$i) { - $xnum = $xnum * $xm1 + $lg_p1[$i]; - $xden = $xden * $xm1 + $lg_q1[$i]; - } - $res = $corr + $xm1 * ($lg_d1 + $xm1 * ($xnum / $xden)); - } else { - $xm2 = $y - 1.0; - $xden = 1.0; - $xnum = 0.0; - for ($i = 0; $i < 8; ++$i) { - $xnum = $xnum * $xm2 + $lg_p2[$i]; - $xden = $xden * $xm2 + $lg_q2[$i]; - } - $res = $corr + $xm2 * ($lg_d2 + $xm2 * ($xnum / $xden)); - } - } elseif ($y <= 4.0) { - // --------------------- - // 1.5 .LT. X .LE. 4.0 - // --------------------- - $xm2 = $y - 2.0; - $xden = 1.0; - $xnum = 0.0; - for ($i = 0; $i < 8; ++$i) { - $xnum = $xnum * $xm2 + $lg_p2[$i]; - $xden = $xden * $xm2 + $lg_q2[$i]; - } - $res = $xm2 * ($lg_d2 + $xm2 * ($xnum / $xden)); - } elseif ($y <= 12.0) { - // ---------------------- - // 4.0 .LT. X .LE. 12.0 - // ---------------------- - $xm4 = $y - 4.0; - $xden = -1.0; - $xnum = 0.0; - for ($i = 0; $i < 8; ++$i) { - $xnum = $xnum * $xm4 + $lg_p4[$i]; - $xden = $xden * $xm4 + $lg_q4[$i]; - } - $res = $lg_d4 + $xm4 * ($xnum / $xden); - } else { - // --------------------------------- - // Evaluate for argument .GE. 12.0 - // --------------------------------- - $res = 0.0; - if ($y <= $lg_frtbig) { - $res = $lg_c[6]; - $ysq = $y * $y; - for ($i = 0; $i < 6; ++$i) { - $res = $res / $ysq + $lg_c[$i]; - } - $res /= $y; - $corr = log($y); - $res = $res + log(self::SQRT2PI) - 0.5 * $corr; - $res += $y * ($corr - 1.0); - } - } - } else { - // -------------------------- - // Return for bad arguments - // -------------------------- - $res = self::MAX_VALUE; - } - // ------------------------------ - // Final adjustments and return - // ------------------------------ - self::$logGammaCacheX = $x; - self::$logGammaCacheResult = $res; - - return $res; - } - - // - // Private implementation of the incomplete Gamma function - // - private static function incompleteGamma($a, $x) - { - static $max = 32; - $summer = 0; - for ($n = 0; $n <= $max; ++$n) { - $divisor = $a; - for ($i = 1; $i <= $n; ++$i) { - $divisor *= ($a + $i); - } - $summer += ($x ** $n / $divisor); - } - - return $x ** $a * exp(0 - $x) * $summer; - } - - // - // Private implementation of the Gamma function - // - private static function gamma($data) - { - if ($data == 0.0) { - return 0; - } - - static $p0 = 1.000000000190015; - static $p = [ - 1 => 76.18009172947146, - 2 => -86.50532032941677, - 3 => 24.01409824083091, - 4 => -1.231739572450155, - 5 => 1.208650973866179e-3, - 6 => -5.395239384953e-6, - ]; - - $y = $x = $data; - $tmp = $x + 5.5; - $tmp -= ($x + 0.5) * log($tmp); - - $summer = $p0; - for ($j = 1; $j <= 6; ++$j) { - $summer += ($p[$j] / ++$y); - } - - return exp(0 - $tmp + log(self::SQRT2PI * $summer / $x)); - } - /* * inverse_ncdf.php * ------------------- @@ -512,16 +116,16 @@ class Statistical * * @Deprecated 1.17.0 * - * @see Statistical\Averages::AVEDEV() - * Use the AVEDEV() method in the Statistical\Averages class instead - * * @param mixed ...$args Data values * * @return float|string + * + *@see Statistical\Averages::averageDeviations() + * Use the averageDeviations() method in the Statistical\Averages class instead */ public static function AVEDEV(...$args) { - return Averages::AVEDEV(...$args); + return Averages::averageDeviations(...$args); } /** @@ -534,8 +138,8 @@ class Statistical * * @Deprecated 1.17.0 * - * @see Statistical\Averages::AVERAGE() - * Use the AVERAGE() method in the Statistical\Averages class instead + * @see Statistical\Averages::average() + * Use the average() method in the Statistical\Averages class instead * * @param mixed ...$args Data values * @@ -543,7 +147,7 @@ class Statistical */ public static function AVERAGE(...$args) { - return Averages::AVERAGE(...$args); + return Averages::average(...$args); } /** @@ -556,16 +160,16 @@ class Statistical * * @Deprecated 1.17.0 * - * @see Statistical\Averages::AVERAGEA() - * Use the AVERAGEA() method in the Statistical\Averages class instead - * * @param mixed ...$args Data values * * @return float|string + * + *@see Statistical\Averages::averageA() + * Use the averageA() method in the Statistical\Averages class instead */ public static function AVERAGEA(...$args) { - return Averages::AVERAGEA(...$args); + return Averages::averageA(...$args); } /** @@ -597,6 +201,11 @@ class Statistical * * Returns the beta distribution. * + * @Deprecated 1.18.0 + * + *@see Statistical\Distributions\Beta::distribution() + * Use the distribution() method in the Statistical\Distributions\Beta class instead + * * @param float $value Value at which you want to evaluate the distribution * @param float $alpha Parameter to the distribution * @param float $beta Parameter to the distribution @@ -607,28 +216,7 @@ class Statistical */ public static function BETADIST($value, $alpha, $beta, $rMin = 0, $rMax = 1) { - $value = Functions::flattenSingleValue($value); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); - $rMin = Functions::flattenSingleValue($rMin); - $rMax = Functions::flattenSingleValue($rMax); - - if ((is_numeric($value)) && (is_numeric($alpha)) && (is_numeric($beta)) && (is_numeric($rMin)) && (is_numeric($rMax))) { - if ($rMin > $rMax) { - $tmp = $rMin; - $rMin = $rMax; - $rMax = $tmp; - } - if (($value < $rMin) || ($value > $rMax) || ($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax)) { - return Functions::NAN(); - } - $value -= $rMin; - $value /= ($rMax - $rMin); - - return self::incompleteBeta($value, $alpha, $beta); - } - - return Functions::VALUE(); + return Statistical\Distributions\Beta::distribution($value, $alpha, $beta, $rMin, $rMax); } /** @@ -636,6 +224,11 @@ class Statistical * * Returns the inverse of the Beta distribution. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Beta::inverse() + * Use the inverse() method in the Statistical\Distributions\Beta class instead + * * @param float $probability Probability at which you want to evaluate the distribution * @param float $alpha Parameter to the distribution * @param float $beta Parameter to the distribution @@ -646,44 +239,7 @@ class Statistical */ public static function BETAINV($probability, $alpha, $beta, $rMin = 0, $rMax = 1) { - $probability = Functions::flattenSingleValue($probability); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); - $rMin = Functions::flattenSingleValue($rMin); - $rMax = Functions::flattenSingleValue($rMax); - - if ((is_numeric($probability)) && (is_numeric($alpha)) && (is_numeric($beta)) && (is_numeric($rMin)) && (is_numeric($rMax))) { - if ($rMin > $rMax) { - $tmp = $rMin; - $rMin = $rMax; - $rMax = $tmp; - } - if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0) || ($probability > 1)) { - return Functions::NAN(); - } - $a = 0; - $b = 2; - - $i = 0; - while ((($b - $a) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { - $guess = ($a + $b) / 2; - $result = self::BETADIST($guess, $alpha, $beta); - if (($result == $probability) || ($result == 0)) { - $b = $a; - } elseif ($result > $probability) { - $b = $guess; - } else { - $a = $guess; - } - } - if ($i == self::MAX_ITERATIONS) { - return Functions::NA(); - } - - return round($rMin + $guess * ($rMax - $rMin), 12); - } - - return Functions::VALUE(); + return Statistical\Distributions\Beta::inverse($probability, $alpha, $beta, $rMin, $rMax); } /** @@ -739,6 +295,11 @@ class Statistical * * Returns the one-tailed probability of the chi-squared distribution. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\ChiSquared::distribution() + * Use the distribution() method in the Statistical\Distributions\ChiSquared class instead + * * @param float $value Value for the function * @param float $degrees degrees of freedom * @@ -746,26 +307,7 @@ class Statistical */ public static function CHIDIST($value, $degrees) { - $value = Functions::flattenSingleValue($value); - $degrees = Functions::flattenSingleValue($degrees); - - if ((is_numeric($value)) && (is_numeric($degrees))) { - $degrees = floor($degrees); - if ($degrees < 1) { - return Functions::NAN(); - } - if ($value < 0) { - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - return 1; - } - - return Functions::NAN(); - } - - return 1 - (self::incompleteGamma($degrees / 2, $value / 2) / self::gamma($degrees / 2)); - } - - return Functions::VALUE(); + return Statistical\Distributions\ChiSquared::distribution($value, $degrees); } /** @@ -773,6 +315,11 @@ class Statistical * * Returns the one-tailed probability of the chi-squared distribution. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\ChiSquared::inverse() + * Use the inverse() method in the Statistical\Distributions\ChiSquared class instead + * * @param float $probability Probability for the function * @param float $degrees degrees of freedom * @@ -780,52 +327,7 @@ class Statistical */ public static function CHIINV($probability, $degrees) { - $probability = Functions::flattenSingleValue($probability); - $degrees = Functions::flattenSingleValue($degrees); - - if ((is_numeric($probability)) && (is_numeric($degrees))) { - $degrees = floor($degrees); - - $xLo = 100; - $xHi = 0; - - $x = $xNew = 1; - $dx = 1; - $i = 0; - - while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { - // Apply Newton-Raphson step - $result = 1 - (self::incompleteGamma($degrees / 2, $x / 2) / self::gamma($degrees / 2)); - $error = $result - $probability; - if ($error == 0.0) { - $dx = 0; - } elseif ($error < 0.0) { - $xLo = $x; - } else { - $xHi = $x; - } - // Avoid division by zero - if ($result != 0.0) { - $dx = $error / $result; - $xNew = $x - $dx; - } - // If the NR fails to converge (which for example may be the - // case if the initial guess is too rough) we apply a bisection - // step to determine a more narrow interval around the root. - if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) { - $xNew = ($xLo + $xHi) / 2; - $dx = $xNew - $x; - } - $x = $xNew; - } - if ($i == self::MAX_ITERATIONS) { - return Functions::NA(); - } - - return round($x, 12); - } - - return Functions::VALUE(); + return Statistical\Distributions\ChiSquared::inverse($probability, $degrees); } /** @@ -1146,7 +648,7 @@ class Statistical // Return value $returnValue = null; - $aMean = Averages::AVERAGE($aArgs); + $aMean = Averages::average($aArgs); if ($aMean != Functions::DIV0()) { $aCount = -1; foreach ($aArgs as $k => $arg) { @@ -1214,16 +716,6 @@ class Statistical return Functions::VALUE(); } - private static function betaFunction($a, $b) - { - return (self::gamma($a) * self::gamma($b)) / self::gamma($a + $b); - } - - private static function regularizedIncompleteBeta($value, $a, $b) - { - return self::incompleteBeta($value, $a, $b) / self::betaFunction($a, $b); - } - /** * F.DIST. * @@ -1259,10 +751,12 @@ class Statistical if ($cumulative) { $adjustedValue = ($u * $value) / ($u * $value + $v); - return self::incompleteBeta($adjustedValue, $u / 2, $v / 2); + return Statistical\Distributions\Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2); } - return (self::gamma(($v + $u) / 2) / (self::gamma($u / 2) * self::gamma($v / 2))) * + return (Statistical\Distributions\Gamma::gammaValue(($v + $u) / 2) / + (Statistical\Distributions\Gamma::gammaValue($u / 2) * + Statistical\Distributions\Gamma::gammaValue($v / 2))) * (($u / $v) ** ($u / 2)) * (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2))); } @@ -1277,23 +771,18 @@ class Statistical * is normally distributed rather than skewed. Use this function to perform hypothesis * testing on the correlation coefficient. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Fisher::distribution() + * Use the distribution() method in the Statistical\Distributions\Fisher class instead + * * @param float $value * * @return float|string */ public static function FISHER($value) { - $value = Functions::flattenSingleValue($value); - - if (is_numeric($value)) { - if (($value <= -1) || ($value >= 1)) { - return Functions::NAN(); - } - - return 0.5 * log((1 + $value) / (1 - $value)); - } - - return Functions::VALUE(); + return Statistical\Distributions\Fisher::distribution($value); } /** @@ -1303,19 +792,18 @@ class Statistical * analyzing correlations between ranges or arrays of data. If y = FISHER(x), then * FISHERINV(y) = x. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Fisher::inverse() + * Use the inverse() method in the Statistical\Distributions\Fisher class instead + * * @param float $value * * @return float|string */ public static function FISHERINV($value) { - $value = Functions::flattenSingleValue($value); - - if (is_numeric($value)) { - return (exp(2 * $value) - 1) / (exp(2 * $value) + 1); - } - - return Functions::VALUE(); + return Statistical\Distributions\Fisher::inverse($value); } /** @@ -1342,7 +830,12 @@ class Statistical /** * GAMMA. * - * Return the gamma function value. + * Returns the gamma function value. + * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Gamma::gamma() + * Use the gamma() method in the Statistical\Distributions\Gamma class instead * * @param float $value * @@ -1350,14 +843,7 @@ class Statistical */ public static function GAMMAFunction($value) { - $value = Functions::flattenSingleValue($value); - if (!is_numeric($value)) { - return Functions::VALUE(); - } elseif ((((int) $value) == ((float) $value)) && $value <= 0.0) { - return Functions::NAN(); - } - - return self::gamma($value); + return Statistical\Distributions\Gamma::gamma($value); } /** @@ -1365,6 +851,11 @@ class Statistical * * Returns the gamma distribution. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Gamma::distribution() + * Use the distribution() method in the Statistical\Distributions\Gamma class instead + * * @param float $value Value at which you want to evaluate the distribution * @param float $a Parameter to the distribution * @param float $b Parameter to the distribution @@ -1374,24 +865,7 @@ class Statistical */ public static function GAMMADIST($value, $a, $b, $cumulative) { - $value = Functions::flattenSingleValue($value); - $a = Functions::flattenSingleValue($a); - $b = Functions::flattenSingleValue($b); - - if ((is_numeric($value)) && (is_numeric($a)) && (is_numeric($b))) { - if (($value < 0) || ($a <= 0) || ($b <= 0)) { - return Functions::NAN(); - } - if ((is_numeric($cumulative)) || (is_bool($cumulative))) { - if ($cumulative) { - return self::incompleteGamma($a, $value / $b) / self::gamma($a); - } - - return (1 / ($b ** $a * self::gamma($a))) * $value ** ($a - 1) * exp(0 - ($value / $b)); - } - } - - return Functions::VALUE(); + return Statistical\Distributions\Gamma::distribution($value, $a, $b, $cumulative); } /** @@ -1399,6 +873,11 @@ class Statistical * * Returns the inverse of the Gamma distribution. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Gamma::inverse() + * Use the inverse() method in the Statistical\Distributions\Gamma class instead + * * @param float $probability Probability at which you want to evaluate the distribution * @param float $alpha Parameter to the distribution * @param float $beta Parameter to the distribution @@ -1407,53 +886,7 @@ class Statistical */ public static function GAMMAINV($probability, $alpha, $beta) { - $probability = Functions::flattenSingleValue($probability); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); - - if ((is_numeric($probability)) && (is_numeric($alpha)) && (is_numeric($beta))) { - if (($alpha <= 0) || ($beta <= 0) || ($probability < 0) || ($probability > 1)) { - return Functions::NAN(); - } - - $xLo = 0; - $xHi = $alpha * $beta * 5; - - $x = $xNew = 1; - $dx = 1024; - $i = 0; - - while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { - // Apply Newton-Raphson step - $error = self::GAMMADIST($x, $alpha, $beta, true) - $probability; - if ($error < 0.0) { - $xLo = $x; - } else { - $xHi = $x; - } - $pdf = self::GAMMADIST($x, $alpha, $beta, false); - // Avoid division by zero - if ($pdf != 0.0) { - $dx = $error / $pdf; - $xNew = $x - $dx; - } - // If the NR fails to converge (which for example may be the - // case if the initial guess is too rough) we apply a bisection - // step to determine a more narrow interval around the root. - if (($xNew < $xLo) || ($xNew > $xHi) || ($pdf == 0.0)) { - $xNew = ($xLo + $xHi) / 2; - $dx = $xNew - $x; - } - $x = $xNew; - } - if ($i == self::MAX_ITERATIONS) { - return Functions::NA(); - } - - return $x; - } - - return Functions::VALUE(); + return Statistical\Distributions\Gamma::inverse($probability, $alpha, $beta); } /** @@ -1461,23 +894,18 @@ class Statistical * * Returns the natural logarithm of the gamma function. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Gamma::ln() + * Use the ln() method in the Statistical\Distributions\Gamma class instead + * * @param float $value * * @return float|string */ public static function GAMMALN($value) { - $value = Functions::flattenSingleValue($value); - - if (is_numeric($value)) { - if ($value <= 0) { - return Functions::NAN(); - } - - return log(self::gamma($value)); - } - - return Functions::VALUE(); + return Statistical\Distributions\Gamma::ln($value); } /** @@ -1673,7 +1101,7 @@ class Statistical public static function KURT(...$args) { $aArgs = Functions::flattenArrayIndexed($args); - $mean = Averages::AVERAGE($aArgs); + $mean = Averages::average($aArgs); $stdDev = StandardDeviations::STDEV($aArgs); if ($stdDev > 0) { @@ -1962,37 +1390,18 @@ class Statistical * Excel Function: * MEDIAN(value1[,value2[, ...]]) * + * @Deprecated 1.18.0 + * + * @see Statistical\Averages::median() + * Use the median() method in the Statistical\Averages class instead + * * @param mixed ...$args Data values * * @return float|string The result, or a string containing an error */ public static function MEDIAN(...$args) { - $returnValue = Functions::NAN(); - - $mArgs = []; - // Loop through arguments - $aArgs = Functions::flattenArray($args); - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $mArgs[] = $arg; - } - } - - $mValueCount = count($mArgs); - if ($mValueCount > 0) { - sort($mArgs, SORT_NUMERIC); - $mValueCount = $mValueCount / 2; - if ($mValueCount == floor($mValueCount)) { - $returnValue = ($mArgs[$mValueCount--] + $mArgs[$mValueCount]) / 2; - } else { - $mValueCount = floor($mValueCount); - $returnValue = $mArgs[$mValueCount]; - } - } - - return $returnValue; + return Statistical\Averages::median(...$args); } /** @@ -2062,55 +1471,6 @@ class Statistical return Conditional::MINIFS(...$args); } - // - // Special variant of array_count_values that isn't limited to strings and integers, - // but can work with floating point numbers as values - // - private static function modeCalc($data) - { - $frequencyArray = []; - $index = 0; - $maxfreq = 0; - $maxfreqkey = ''; - $maxfreqdatum = ''; - foreach ($data as $datum) { - $found = false; - ++$index; - foreach ($frequencyArray as $key => $value) { - if ((string) $value['value'] == (string) $datum) { - ++$frequencyArray[$key]['frequency']; - $freq = $frequencyArray[$key]['frequency']; - if ($freq > $maxfreq) { - $maxfreq = $freq; - $maxfreqkey = $key; - $maxfreqdatum = $datum; - } elseif ($freq == $maxfreq) { - if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { - $maxfreqkey = $key; - $maxfreqdatum = $datum; - } - } - $found = true; - - break; - } - } - if (!$found) { - $frequencyArray[] = [ - 'value' => $datum, - 'frequency' => 1, - 'index' => $index, - ]; - } - } - - if ($maxfreq <= 1) { - return Functions::NA(); - } - - return $maxfreqdatum; - } - /** * MODE. * @@ -2119,30 +1479,18 @@ class Statistical * Excel Function: * MODE(value1[,value2[, ...]]) * + * @Deprecated 1.18.0 + * + * @see Statistical\Averages::mode() + * Use the mode() method in the Statistical\Averages class instead + * * @param mixed ...$args Data values * * @return float|string The result, or a string containing an error */ public static function MODE(...$args) { - $returnValue = Functions::NA(); - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - - $mArgs = []; - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $mArgs[] = $arg; - } - } - - if (!empty($mArgs)) { - return self::modeCalc($mArgs); - } - - return $returnValue; + return Statistical\Averages::mode(...$args); } /** @@ -2575,7 +1923,7 @@ class Statistical public static function SKEW(...$args) { $aArgs = Functions::flattenArrayIndexed($args); - $mean = Averages::AVERAGE($aArgs); + $mean = Averages::average($aArgs); $stdDev = StandardDeviations::STDEV($aArgs); if ($stdDev === 0.0 || is_string($stdDev)) { @@ -2990,7 +2338,7 @@ class Statistical array_shift($mArgs); } - return Averages::AVERAGE($mArgs); + return Averages::average($mArgs); } return Functions::VALUE(); @@ -3142,6 +2490,6 @@ class Statistical } $n = count($dataSet); - return 1 - self::NORMSDIST((Averages::AVERAGE($dataSet) - $m0) / ($sigma / sqrt($n))); + return 1 - self::NORMSDIST((Averages::average($dataSet) - $m0) / ($sigma / sqrt($n))); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php index 14c9fef2..1a627e99 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Averages.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Averages.php @@ -19,14 +19,14 @@ class Averages extends AggregateBase * * @return float|string (string if result is an error) */ - public static function AVEDEV(...$args) + public static function averageDeviations(...$args) { $aArgs = Functions::flattenArrayIndexed($args); // Return value $returnValue = 0; - $aMean = self::AVERAGE(...$args); + $aMean = self::average(...$args); if ($aMean === Functions::DIV0()) { return Functions::NAN(); } elseif ($aMean === Functions::VALUE()) { @@ -68,7 +68,7 @@ class Averages extends AggregateBase * * @return float|string (string if result is an error) */ - public static function AVERAGE(...$args) + public static function average(...$args) { $returnValue = $aCount = 0; @@ -107,7 +107,7 @@ class Averages extends AggregateBase * * @return float|string (string if result is an error) */ - public static function AVERAGEA(...$args) + public static function averageA(...$args) { $returnValue = null; @@ -134,4 +134,126 @@ class Averages extends AggregateBase return Functions::DIV0(); } + + /** + * MEDIAN. + * + * Returns the median of the given numbers. The median is the number in the middle of a set of numbers. + * + * Excel Function: + * MEDIAN(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function median(...$args) + { + $aArgs = Functions::flattenArray($args); + + $returnValue = Functions::NAN(); + + $aArgs = self::filterArguments($aArgs); + $valueCount = count($aArgs); + if ($valueCount > 0) { + sort($aArgs, SORT_NUMERIC); + $valueCount = $valueCount / 2; + if ($valueCount == floor($valueCount)) { + $returnValue = ($aArgs[$valueCount--] + $aArgs[$valueCount]) / 2; + } else { + $valueCount = floor($valueCount); + $returnValue = $aArgs[$valueCount]; + } + } + + return $returnValue; + } + + /** + * MODE. + * + * Returns the most frequently occurring, or repetitive, value in an array or range of data + * + * Excel Function: + * MODE(value1[,value2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function mode(...$args) + { + $returnValue = Functions::NA(); + + // Loop through arguments + $aArgs = Functions::flattenArray($args); + $aArgs = self::filterArguments($aArgs); + + if (!empty($aArgs)) { + return self::modeCalc($aArgs); + } + + return $returnValue; + } + + protected static function filterArguments($args) + { + return array_filter( + $args, + function ($value) { + // Is it a numeric value? + return (is_numeric($value)) && (!is_string($value)); + } + ); + } + + // + // Special variant of array_count_values that isn't limited to strings and integers, + // but can work with floating point numbers as values + // + private static function modeCalc($data) + { + $frequencyArray = []; + $index = 0; + $maxfreq = 0; + $maxfreqkey = ''; + $maxfreqdatum = ''; + foreach ($data as $datum) { + $found = false; + ++$index; + foreach ($frequencyArray as $key => $value) { + if ((string) $value['value'] == (string) $datum) { + ++$frequencyArray[$key]['frequency']; + $freq = $frequencyArray[$key]['frequency']; + if ($freq > $maxfreq) { + $maxfreq = $freq; + $maxfreqkey = $key; + $maxfreqdatum = $datum; + } elseif ($freq == $maxfreq) { + if ($frequencyArray[$key]['index'] < $frequencyArray[$maxfreqkey]['index']) { + $maxfreqkey = $key; + $maxfreqdatum = $datum; + } + } + $found = true; + + break; + } + } + + if ($found === false) { + $frequencyArray[] = [ + 'value' => $datum, + 'frequency' => 1, + 'index' => $index, + ]; + } + } + + if ($maxfreq <= 1) { + return Functions::NA(); + } + + return $maxfreqdatum; + } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php index c4c2a7dd..3147859b 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php @@ -12,9 +12,9 @@ class Confidence * * Returns the confidence interval for a population mean * - * @param float $alpha - * @param float $stdDev Standard Deviation - * @param float $size + * @param mixed (float) $alpha + * @param mixed (float) $stdDev Standard Deviation + * @param mixed (float) $size * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php new file mode 100644 index 00000000..a8ab3e89 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php @@ -0,0 +1,36 @@ +getMessage(); + } + + if ($rMin > $rMax) { + $tmp = $rMin; + $rMin = $rMax; + $rMax = $tmp; + } + if (($value < $rMin) || ($value > $rMax) || ($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax)) { + return Functions::NAN(); + } + + $value -= $rMin; + $value /= ($rMax - $rMin); + + return self::incompleteBeta($value, $alpha, $beta); + } + + /** + * BETAINV. + * + * Returns the inverse of the Beta distribution. + * + * @param mixed (float) $probability Probability at which you want to evaluate the distribution + * @param mixed (float) $alpha Parameter to the distribution + * @param mixed (float) $beta Parameter to the distribution + * @param mixed (float) $rMin Minimum value + * @param mixed (float) $rMax Maximum value + * + * @return float|string + */ + public static function inverse($probability, $alpha, $beta, $rMin = 0, $rMax = 1) + { + $probability = Functions::flattenSingleValue($probability); + $alpha = Functions::flattenSingleValue($alpha); + $beta = Functions::flattenSingleValue($beta); + $rMin = Functions::flattenSingleValue($rMin); + $rMax = Functions::flattenSingleValue($rMax); + + try { + $probability = self::validateFloat($probability); + $alpha = self::validateFloat($alpha); + $beta = self::validateFloat($beta); + $rMax = self::validateFloat($rMax); + $rMin = self::validateFloat($rMin); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($rMin > $rMax) { + $tmp = $rMin; + $rMin = $rMax; + $rMax = $tmp; + } + if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0) || ($probability > 1)) { + return Functions::NAN(); + } + + return self::calculateInverse($probability, $alpha, $beta, $rMin, $rMax); + } + + private static function calculateInverse(float $probability, float $alpha, float $beta, float $rMin, float $rMax) + { + $a = 0; + $b = 2; + + $i = 0; + while ((($b - $a) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { + $guess = ($a + $b) / 2; + $result = self::distribution($guess, $alpha, $beta); + if (($result === $probability) || ($result === 0.0)) { + $b = $a; + } elseif ($result > $probability) { + $b = $guess; + } else { + $a = $guess; + } + } + + if ($i === self::MAX_ITERATIONS) { + return Functions::NA(); + } + + return round($rMin + $guess * ($rMax - $rMin), 12); + } + + /** + * Incomplete beta function. + * + * @author Jaco van Kooten + * @author Paul Meagher + * + * The computation is based on formulas from Numerical Recipes, Chapter 6.4 (W.H. Press et al, 1992). + * + * @param mixed $x require 0<=x<=1 + * @param mixed $p require p>0 + * @param mixed $q require q>0 + * + * @return float 0 if x<0, p<=0, q<=0 or p+q>2.55E305 and 1 if x>1 to avoid errors and over/underflow + */ + public static function incompleteBeta(float $x, float $p, float $q): float + { + if ($x <= 0.0) { + return 0.0; + } elseif ($x >= 1.0) { + return 1.0; + } elseif (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) { + return 0.0; + } + + $beta_gam = exp((0 - self::logBeta($p, $q)) + $p * log($x) + $q * log(1.0 - $x)); + if ($x < ($p + 1.0) / ($p + $q + 2.0)) { + return $beta_gam * self::betaFraction($x, $p, $q) / $p; + } + + return 1.0 - ($beta_gam * self::betaFraction(1 - $x, $q, $p) / $q); + } + + // Function cache for logBeta function + private static $logBetaCacheP = 0.0; + + private static $logBetaCacheQ = 0.0; + + private static $logBetaCacheResult = 0.0; + + /** + * The natural logarithm of the beta function. + * + * @param mixed $p require p>0 + * @param mixed $q require q>0 + * + * @return float 0 if p<=0, q<=0 or p+q>2.55E305 to avoid errors and over/underflow + * + * @author Jaco van Kooten + */ + private static function logBeta(float $p, float $q): float + { + if ($p != self::$logBetaCacheP || $q != self::$logBetaCacheQ) { + self::$logBetaCacheP = $p; + self::$logBetaCacheQ = $q; + if (($p <= 0.0) || ($q <= 0.0) || (($p + $q) > self::LOG_GAMMA_X_MAX_VALUE)) { + self::$logBetaCacheResult = 0.0; + } else { + self::$logBetaCacheResult = Gamma::logGamma($p) + Gamma::logGamma($q) - Gamma::logGamma($p + $q); + } + } + + return self::$logBetaCacheResult; + } + + /** + * Evaluates of continued fraction part of incomplete beta function. + * Based on an idea from Numerical Recipes (W.H. Press et al, 1992). + * + * @author Jaco van Kooten + * + * @param mixed $x + * @param mixed $p + * @param mixed $q + */ + private static function betaFraction(float $x, float $p, float $q): float + { + $c = 1.0; + $sum_pq = $p + $q; + $p_plus = $p + 1.0; + $p_minus = $p - 1.0; + $h = 1.0 - $sum_pq * $x / $p_plus; + if (abs($h) < self::XMININ) { + $h = self::XMININ; + } + $h = 1.0 / $h; + $frac = $h; + $m = 1; + $delta = 0.0; + while ($m <= self::MAX_ITERATIONS && abs($delta - 1.0) > Functions::PRECISION) { + $m2 = 2 * $m; + // even index for d + $d = $m * ($q - $m) * $x / (($p_minus + $m2) * ($p + $m2)); + $h = 1.0 + $d * $h; + if (abs($h) < self::XMININ) { + $h = self::XMININ; + } + $h = 1.0 / $h; + $c = 1.0 + $d / $c; + if (abs($c) < self::XMININ) { + $c = self::XMININ; + } + $frac *= $h * $c; + // odd index for d + $d = -($p + $m) * ($sum_pq + $m) * $x / (($p + $m2) * ($p_plus + $m2)); + $h = 1.0 + $d * $h; + if (abs($h) < self::XMININ) { + $h = self::XMININ; + } + $h = 1.0 / $h; + $c = 1.0 + $d / $c; + if (abs($c) < self::XMININ) { + $c = self::XMININ; + } + $delta = $h * $c; + $frac *= $delta; + ++$m; + } + + return $frac; + } + + private static function betaValue(float $a, float $b): float + { + return (Gamma::gammaValue($a) * Gamma::gammaValue($b)) / + Gamma::gammaValue($a + $b); + } + + private static function regularizedIncompleteBeta(float $value, float $a, float $b): float + { + return self::incompleteBeta($value, $a, $b) / self::betaValue($a, $b); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php new file mode 100644 index 00000000..2d5e4496 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -0,0 +1,127 @@ +getMessage(); + } + + if ($degrees < 1) { + return Functions::NAN(); + } + if ($value < 0) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + return 1; + } + + return Functions::NAN(); + } + + return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2)); + } + + /** + * CHIINV. + * + * Returns the one-tailed probability of the chi-squared distribution. + * + * @param mixed (float) $probability Probability for the function + * @param mixed (int) $degrees degrees of freedom + * + * @return float|string + */ + public static function inverse($probability, $degrees) + { + $probability = Functions::flattenSingleValue($probability); + $degrees = Functions::flattenSingleValue($degrees); + + try { + $probability = self::validateFloat($probability); + $degrees = self::validateInt($degrees); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($probability < 0.0 || $probability > 1.0 || $degrees < 1) { + return Functions::NAN(); + } + + return self::calculateInverse($degrees, $probability); + } + + /** + * @return float|string + */ + protected static function calculateInverse(int $degrees, float $probability) + { + $xLo = 100; + $xHi = 0; + + $x = $xNew = 1; + $dx = 1; + $i = 0; + + while ((abs($dx) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { + // Apply Newton-Raphson step + $result = 1 - (Gamma::incompleteGamma($degrees / 2, $x / 2) + / Gamma::gammaValue($degrees / 2)); + $error = $result - $probability; + + if ($error == 0.0) { + $dx = 0; + } elseif ($error < 0.0) { + $xLo = $x; + } else { + $xHi = $x; + } + + // Avoid division by zero + if ($result != 0.0) { + $dx = $error / $result; + $xNew = $x - $dx; + } + + // If the NR fails to converge (which for example may be the + // case if the initial guess is too rough) we apply a bisection + // step to determine a more narrow interval around the root. + if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) { + $xNew = ($xLo + $xHi) / 2; + $dx = $xNew - $x; + } + $x = $xNew; + } + + if ($i === self::MAX_ITERATIONS) { + return Functions::NA(); + } + + return $x; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php new file mode 100644 index 00000000..1d3a7be4 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php @@ -0,0 +1,63 @@ +getMessage(); + } + + if (($value <= -1) || ($value >= 1)) { + return Functions::NAN(); + } + + return 0.5 * log((1 + $value) / (1 - $value)); + } + + /** + * FISHERINV. + * + * Returns the inverse of the Fisher transformation. Use this transformation when + * analyzing correlations between ranges or arrays of data. If y = FISHER(x), then + * FISHERINV(y) = x. + * + * @param mixed (float) $value + * + * @return float|string + */ + public static function inverse($value) + { + $value = Functions::flattenSingleValue($value); + + try { + self::validateFloat($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + return (exp(2 * $value) - 1) / (exp(2 * $value) + 1); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php new file mode 100644 index 00000000..aa487329 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php @@ -0,0 +1,129 @@ +getMessage(); + } + + if ((((int) $value) == ((float) $value)) && $value <= 0.0) { + return Functions::NAN(); + } + + return self::gammaValue($value); + } + + /** + * GAMMADIST. + * + * Returns the gamma distribution. + * + * @param mixed (float) $value Value at which you want to evaluate the distribution + * @param mixed (float) $a Parameter to the distribution + * @param mixed (float) $b Parameter to the distribution + * @param mixed (bool) $cumulative + * + * @return float|string + */ + public static function distribution($value, $a, $b, $cumulative) + { + $value = Functions::flattenSingleValue($value); + $a = Functions::flattenSingleValue($a); + $b = Functions::flattenSingleValue($b); + + try { + $value = self::validateFloat($value); + $a = self::validateFloat($a); + $b = self::validateFloat($b); + $cumulative = self::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value < 0) || ($a <= 0) || ($b <= 0)) { + return Functions::NAN(); + } + + return self::calculateDistribution($value, $a, $b, $cumulative); + } + + /** + * GAMMAINV. + * + * Returns the inverse of the Gamma distribution. + * + * @param mixed (float) $probability Probability at which you want to evaluate the distribution + * @param mixed (float) $alpha Parameter to the distribution + * @param mixed (float) $beta Parameter to the distribution + * + * @return float|string + */ + public static function inverse($probability, $alpha, $beta) + { + $probability = Functions::flattenSingleValue($probability); + $alpha = Functions::flattenSingleValue($alpha); + $beta = Functions::flattenSingleValue($beta); + + try { + $probability = self::validateFloat($probability); + $alpha = self::validateFloat($alpha); + $beta = self::validateFloat($beta); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($alpha <= 0.0) || ($beta <= 0.0) || ($probability < 0.0) || ($probability > 1.0)) { + return Functions::NAN(); + } + + return self::calculateInverse($probability, $alpha, $beta); + } + + /** + * GAMMALN. + * + * Returns the natural logarithm of the gamma function. + * + * @param mixed (float) $value + * + * @return float|string + */ + public static function ln($value) + { + $value = Functions::flattenSingleValue($value); + + try { + $value = self::validateFloat($value); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($value <= 0) { + return Functions::NAN(); + } + + return log(self::gammaValue($value)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php new file mode 100644 index 00000000..ae951af3 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php @@ -0,0 +1,377 @@ + Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { + // Apply Newton-Raphson step + $error = self::calculateDistribution($x, $alpha, $beta, true) - $probability; + if ($error < 0.0) { + $xLo = $x; + } else { + $xHi = $x; + } + + $pdf = self::calculateDistribution($x, $alpha, $beta, false); + // Avoid division by zero + if ($pdf !== 0.0) { + $dx = $error / $pdf; + $xNew = $x - $dx; + } + + // If the NR fails to converge (which for example may be the + // case if the initial guess is too rough) we apply a bisection + // step to determine a more narrow interval around the root. + if (($xNew < $xLo) || ($xNew > $xHi) || ($pdf == 0.0)) { + $xNew = ($xLo + $xHi) / 2; + $dx = $xNew - $x; + } + $x = $xNew; + } + + if ($i === self::MAX_ITERATIONS) { + return Functions::NA(); + } + + return $x; + } + + // + // Implementation of the incomplete Gamma function + // + public static function incompleteGamma(float $a, float $x): float + { + static $max = 32; + $summer = 0; + for ($n = 0; $n <= $max; ++$n) { + $divisor = $a; + for ($i = 1; $i <= $n; ++$i) { + $divisor *= ($a + $i); + } + $summer += ($x ** $n / $divisor); + } + + return $x ** $a * exp(0 - $x) * $summer; + } + + // + // Implementation of the Gamma function + // + public static function gammaValue(float $value): float + { + if ($value == 0.0) { + return 0; + } + + static $p0 = 1.000000000190015; + static $p = [ + 1 => 76.18009172947146, + 2 => -86.50532032941677, + 3 => 24.01409824083091, + 4 => -1.231739572450155, + 5 => 1.208650973866179e-3, + 6 => -5.395239384953e-6, + ]; + + $y = $x = $value; + $tmp = $x + 5.5; + $tmp -= ($x + 0.5) * log($tmp); + + $summer = $p0; + for ($j = 1; $j <= 6; ++$j) { + $summer += ($p[$j] / ++$y); + } + + return exp(0 - $tmp + log(self::SQRT2PI * $summer / $x)); + } + + /** + * logGamma function. + * + * @version 1.1 + * + * @author Jaco van Kooten + * + * Original author was Jaco van Kooten. Ported to PHP by Paul Meagher. + * + * The natural logarithm of the gamma function.
+ * Based on public domain NETLIB (Fortran) code by W. J. Cody and L. Stoltz
+ * Applied Mathematics Division
+ * Argonne National Laboratory
+ * Argonne, IL 60439
+ *

+ * References: + *

    + *
  1. W. J. Cody and K. E. Hillstrom, 'Chebyshev Approximations for the Natural + * Logarithm of the Gamma Function,' Math. Comp. 21, 1967, pp. 198-203.
  2. + *
  3. K. E. Hillstrom, ANL/AMD Program ANLC366S, DGAMMA/DLGAMA, May, 1969.
  4. + *
  5. Hart, Et. Al., Computer Approximations, Wiley and sons, New York, 1968.
  6. + *
+ *

+ *

+ * From the original documentation: + *

+ *

+ * This routine calculates the LOG(GAMMA) function for a positive real argument X. + * Computation is based on an algorithm outlined in references 1 and 2. + * The program uses rational functions that theoretically approximate LOG(GAMMA) + * to at least 18 significant decimal digits. The approximation for X > 12 is from + * reference 3, while approximations for X < 12.0 are similar to those in reference + * 1, but are unpublished. The accuracy achieved depends on the arithmetic system, + * the compiler, the intrinsic functions, and proper selection of the + * machine-dependent constants. + *

+ *

+ * Error returns:
+ * The program returns the value XINF for X .LE. 0.0 or when overflow would occur. + * The computation is believed to be free of underflow and overflow. + *

+ * + * @return float MAX_VALUE for x < 0.0 or when overflow would occur, i.e. x > 2.55E305 + */ + + // Log Gamma related constants + private const LG_D1 = -0.5772156649015328605195174; + + private const LG_D2 = 0.4227843350984671393993777; + + private const LG_D4 = 1.791759469228055000094023; + + private const LG_P1 = [ + 4.945235359296727046734888, + 201.8112620856775083915565, + 2290.838373831346393026739, + 11319.67205903380828685045, + 28557.24635671635335736389, + 38484.96228443793359990269, + 26377.48787624195437963534, + 7225.813979700288197698961, + ]; + + private const LG_P2 = [ + 4.974607845568932035012064, + 542.4138599891070494101986, + 15506.93864978364947665077, + 184793.2904445632425417223, + 1088204.76946882876749847, + 3338152.967987029735917223, + 5106661.678927352456275255, + 3074109.054850539556250927, + ]; + + private const LG_P4 = [ + 14745.02166059939948905062, + 2426813.369486704502836312, + 121475557.4045093227939592, + 2663432449.630976949898078, + 29403789566.34553899906876, + 170266573776.5398868392998, + 492612579337.743088758812, + 560625185622.3951465078242, + ]; + + private const LG_Q1 = [ + 67.48212550303777196073036, + 1113.332393857199323513008, + 7738.757056935398733233834, + 27639.87074403340708898585, + 54993.10206226157329794414, + 61611.22180066002127833352, + 36351.27591501940507276287, + 8785.536302431013170870835, + ]; + + private const LG_Q2 = [ + 183.0328399370592604055942, + 7765.049321445005871323047, + 133190.3827966074194402448, + 1136705.821321969608938755, + 5267964.117437946917577538, + 13467014.54311101692290052, + 17827365.30353274213975932, + 9533095.591844353613395747, + ]; + + private const LG_Q4 = [ + 2690.530175870899333379843, + 639388.5654300092398984238, + 41355999.30241388052042842, + 1120872109.61614794137657, + 14886137286.78813811542398, + 101680358627.2438228077304, + 341747634550.7377132798597, + 446315818741.9713286462081, + ]; + + private const LG_C = [ + -0.001910444077728, + 8.4171387781295e-4, + -5.952379913043012e-4, + 7.93650793500350248e-4, + -0.002777777777777681622553, + 0.08333333333333333331554247, + 0.0057083835261, + ]; + + // Rough estimate of the fourth root of logGamma_xBig + private const LG_FRTBIG = 2.25e76; + + private const PNT68 = 0.6796875; + + // Function cache for logGamma + private static $logGammaCacheResult = 0.0; + + private static $logGammaCacheX = 0.0; + + public static function logGamma(float $x): float + { + if ($x == self::$logGammaCacheX) { + return self::$logGammaCacheResult; + } + + $y = $x; + if ($y > 0.0 && $y <= self::LOG_GAMMA_X_MAX_VALUE) { + if ($y <= self::EPS) { + $res = -log($y); + } elseif ($y <= 1.5) { + $res = self::logGamma1($y); + } elseif ($y <= 4.0) { + $res = self::logGamma2($y); + } elseif ($y <= 12.0) { + $res = self::logGamma3($y); + } else { + $res = self::logGamma4($y); + } + } else { + // -------------------------- + // Return for bad arguments + // -------------------------- + $res = self::MAX_VALUE; + } + + // ------------------------------ + // Final adjustments and return + // ------------------------------ + self::$logGammaCacheX = $x; + self::$logGammaCacheResult = $res; + + return $res; + } + + private static function logGamma1(float $y) + { + // --------------------- + // EPS .LT. X .LE. 1.5 + // --------------------- + if ($y < self::PNT68) { + $corr = -log($y); + $xm1 = $y; + } else { + $corr = 0.0; + $xm1 = $y - 1.0; + } + + $xden = 1.0; + $xnum = 0.0; + if ($y <= 0.5 || $y >= self::PNT68) { + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm1 + self::LG_P1[$i]; + $xden = $xden * $xm1 + self::LG_Q1[$i]; + } + + return $corr + $xm1 * (self::LG_D1 + $xm1 * ($xnum / $xden)); + } + + $xm2 = $y - 1.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm2 + self::LG_P2[$i]; + $xden = $xden * $xm2 + self::LG_Q2[$i]; + } + + return $corr + $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden)); + } + + private static function logGamma2(float $y) + { + // --------------------- + // 1.5 .LT. X .LE. 4.0 + // --------------------- + $xm2 = $y - 2.0; + $xden = 1.0; + $xnum = 0.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm2 + self::LG_P2[$i]; + $xden = $xden * $xm2 + self::LG_Q2[$i]; + } + + return $xm2 * (self::LG_D2 + $xm2 * ($xnum / $xden)); + } + + protected static function logGamma3(float $y) + { + // ---------------------- + // 4.0 .LT. X .LE. 12.0 + // ---------------------- + $xm4 = $y - 4.0; + $xden = -1.0; + $xnum = 0.0; + for ($i = 0; $i < 8; ++$i) { + $xnum = $xnum * $xm4 + self::LG_P4[$i]; + $xden = $xden * $xm4 + self::LG_Q4[$i]; + } + + return self::LG_D4 + $xm4 * ($xnum / $xden); + } + + protected static function logGamma4(float $y) + { + // --------------------------------- + // Evaluate for argument .GE. 12.0 + // --------------------------------- + $res = 0.0; + if ($y <= self::LG_FRTBIG) { + $res = self::LG_C[6]; + $ysq = $y * $y; + for ($i = 0; $i < 6; ++$i) { + $res = $res / $ysq + self::LG_C[$i]; + } + $res /= $y; + $corr = log($y); + $res = $res + log(self::SQRT2PI) - 0.5 * $corr; + $res += $y * ($corr - 1.0); + } + + return $res; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php index 84c10719..5d03e5d5 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -16,8 +16,8 @@ class Permutations * combinations, for which the internal order is not significant. Use this function * for lottery-style probability calculations. * - * @param int $numObjs Number of different objects - * @param int $numInSet Number of objects in each permutation + * @param mixed (int) $numObjs Number of different objects + * @param mixed (int) $numInSet Number of objects in each permutation * * @return int|string Number of permutations, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php b/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php index 28a25a75..4f15615c 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php @@ -23,7 +23,7 @@ class StandardDeviations extends VarianceBase { $aArgs = Functions::flattenArrayIndexed($args); - $aMean = Averages::AVERAGE($aArgs); + $aMean = Averages::average($aArgs); if (!is_string($aMean)) { $returnValue = 0.0; @@ -67,7 +67,7 @@ class StandardDeviations extends VarianceBase { $aArgs = Functions::flattenArrayIndexed($args); - $aMean = Averages::AVERAGEA($aArgs); + $aMean = Averages::averageA($aArgs); if (!is_string($aMean)) { $returnValue = 0.0; @@ -109,7 +109,7 @@ class StandardDeviations extends VarianceBase { $aArgs = Functions::flattenArrayIndexed($args); - $aMean = Averages::AVERAGE($aArgs); + $aMean = Averages::average($aArgs); if (!is_string($aMean)) { $returnValue = 0.0; @@ -153,7 +153,7 @@ class StandardDeviations extends VarianceBase { $aArgs = Functions::flattenArrayIndexed($args); - $aMean = Averages::AVERAGEA($aArgs); + $aMean = Averages::averageA($aArgs); if (!is_string($aMean)) { $returnValue = 0.0; diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index a1137cef..b1dfbaef 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -107,7 +107,7 @@ class Trends * Calculates, or predicts, a future value by using existing values. * The predicted value is a y-value for a given x-value. * - * @param float $xValue Value of X for which we want to find Y + * @param mixed (float) $xValue Value of X for which we want to find Y * @param mixed $yValues array of mixed Data Series Y * @param mixed $xValues of mixed Data Series X * @@ -140,7 +140,7 @@ class Trends * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @param mixed[] $newValues Values of X for which we want to find Y - * @param bool $const a logical value specifying whether to force the intersect to equal 0 + * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 * * @return array of float */ @@ -196,8 +196,8 @@ class Trends * * @param mixed[] $yValues Data Series Y * @param null|mixed[] $xValues Data Series X - * @param bool $const a logical value specifying whether to force the intersect to equal 0 - * @param bool $stats a logical value specifying whether to return additional regression statistics + * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 + * @param mixed (bool) $stats a logical value specifying whether to return additional regression statistics * * @return array|int|string The result, or a string containing an error */ @@ -257,8 +257,8 @@ class Trends * * @param mixed[] $yValues Data Series Y * @param null|mixed[] $xValues Data Series X - * @param bool $const a logical value specifying whether to force the intersect to equal 0 - * @param bool $stats a logical value specifying whether to return additional regression statistics + * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 + * @param mixed (bool) $stats a logical value specifying whether to return additional regression statistics * * @return array|int|string The result, or a string containing an error */ @@ -397,7 +397,7 @@ class Trends * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @param mixed[] $newValues Values of X for which we want to find Y - * @param bool $const a logical value specifying whether to force the intersect to equal 0 + * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 * * @return array of float */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php index 2a275133..846a3124 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php @@ -13,7 +13,7 @@ class CaseConvert * * Converts a string value to upper case. * - * @param string $mixedCaseValue + * @param mixed (string) $mixedCaseValue */ public static function lower($mixedCaseValue): string { @@ -31,7 +31,7 @@ class CaseConvert * * Converts a string value to upper case. * - * @param string $mixedCaseValue + * @param mixed (string) $mixedCaseValue */ public static function upper($mixedCaseValue): string { @@ -49,7 +49,7 @@ class CaseConvert * * Converts a string value to upper case. * - * @param string $mixedCaseValue + * @param mixed (string) $mixedCaseValue */ public static function proper($mixedCaseValue): string { diff --git a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php index 2263b1a7..0003e0cd 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php @@ -10,7 +10,7 @@ class CharacterConvert /** * CHARACTER. * - * @param string $character Value + * @param mixed (int) $character Value */ public static function character($character): string { @@ -31,7 +31,7 @@ class CharacterConvert /** * ASCIICODE. * - * @param string $characters Value + * @param mixed (string) $characters Value * * @return int|string A string if arguments are invalid */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 126d9f49..7ef76546 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -10,8 +10,8 @@ class Extract /** * LEFT. * - * @param string $value Value - * @param int $chars Number of characters + * @param mixed (string) $value Value + * @param mixed (int) $chars Number of characters */ public static function left($value = '', $chars = 1): string { @@ -32,9 +32,9 @@ class Extract /** * MID. * - * @param string $value Value - * @param int $start Start character - * @param int $chars Number of characters + * @param mixed (string) $value Value + * @param mixed (int) $start Start character + * @param mixed (int) $chars Number of characters */ public static function mid($value = '', $start = 1, $chars = null): string { @@ -56,8 +56,8 @@ class Extract /** * RIGHT. * - * @param string $value Value - * @param int $chars Number of characters + * @param mixed (string) $value Value + * @param mixed (int) $chars Number of characters */ public static function right($value = '', $chars = 1): string { diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index 2cea474d..f24ed7ae 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -18,8 +18,8 @@ class Format * 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 float $value The value to format - * @param int $decimals The number of digits to display to the right of the decimal point. + * @param mixed (float) $value The value to format + * @param mixed (int) $decimals The number of digits to display to the right of the decimal point. * If decimals is negative, number is rounded to the left of the decimal point. * If you omit decimals, it is assumed to be 2 */ @@ -54,7 +54,7 @@ class Format * * @param mixed $value Value to check * @param mixed $decimals - * @param bool $noCommas + * @param mixed (bool) $noCommas */ public static function FIXEDFORMAT($value, $decimals = 2, $noCommas = false): string { @@ -72,7 +72,7 @@ class Format if ($decimals < 0) { $decimals = 0; } - if (!$noCommas) { + if ($noCommas === false) { $valueResult = number_format( $valueResult, $decimals, @@ -88,7 +88,7 @@ class Format * TEXTFORMAT. * * @param mixed $value Value to check - * @param string $format Format mask to use + * @param mixed (string) $format Format mask to use */ public static function TEXTFORMAT($value, $format): string { @@ -152,8 +152,8 @@ class Format * NUMBERVALUE. * * @param mixed $value Value to check - * @param string $decimalSeparator decimal separator, defaults to locale defined value - * @param string $groupSeparator group/thosands separator, defaults to locale defined value + * @param mixed (string) $decimalSeparator decimal separator, defaults to locale defined value + * @param mixed (string) $groupSeparator group/thosands separator, defaults to locale defined value * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/Replace.php b/src/PhpSpreadsheet/Calculation/TextData/Replace.php index 9a849ba0..a06d4364 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Replace.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Replace.php @@ -10,10 +10,10 @@ class Replace /** * REPLACE. * - * @param string $oldText String to modify - * @param int $start Start character - * @param int $chars Number of characters - * @param string $newText String to replace in defined position + * @param mixed (string) $oldText String to modify + * @param mixed (int) $start Start character + * @param mixed (int) $chars Number of characters + * @param mixed (string) $newText String to replace in defined position */ public static function replace($oldText, $start, $chars, $newText): string { @@ -31,10 +31,10 @@ class Replace /** * SUBSTITUTE. * - * @param string $text Value - * @param string $fromText From Value - * @param string $toText To Value - * @param int $instance Instance Number + * @param mixed (string) $text Value + * @param mixed (string) $fromText From Value + * @param mixed (string) $toText To Value + * @param mixed (int) $instance Instance Number */ public static function substitute($text = '', $fromText = '', $toText = '', $instance = 0): string { diff --git a/src/PhpSpreadsheet/Calculation/TextData/Search.php b/src/PhpSpreadsheet/Calculation/TextData/Search.php index acbe6a24..cf1bf241 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Search.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Search.php @@ -11,9 +11,9 @@ class Search /** * SEARCHSENSITIVE. * - * @param string $needle The string to look for - * @param string $haystack The string in which to look - * @param int $offset Offset within $haystack + * @param mixed (string) $needle The string to look for + * @param mixed (string) $haystack The string in which to look + * @param mixed (int) $offset Offset within $haystack * * @return int|string */ @@ -46,9 +46,9 @@ class Search /** * SEARCHINSENSITIVE. * - * @param string $needle The string to look for - * @param string $haystack The string in which to look - * @param int $offset Offset within $haystack + * @param mixed (string) $needle The string to look for + * @param mixed (string) $haystack The string in which to look + * @param mixed (int) $offset Offset within $haystack * * @return int|string */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index a47d373b..338cdd20 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -10,7 +10,7 @@ class Text /** * STRINGLENGTH. * - * @param string $value Value + * @param mixed (string) $value Value */ public static function length($value = ''): int { @@ -28,8 +28,8 @@ class Text * EXACT is case-sensitive but ignores formatting differences. * Use EXACT to test text being entered into a document. * - * @param $value1 - * @param $value2 + * @param mixed (string) $value1 + * @param mixed (string) $value2 */ public static function exact($value1, $value2): bool { diff --git a/src/PhpSpreadsheet/Calculation/TextData/Trim.php b/src/PhpSpreadsheet/Calculation/TextData/Trim.php index 0d5688b0..01fff1a8 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Trim.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Trim.php @@ -12,7 +12,7 @@ class Trim /** * TRIMNONPRINTABLE. * - * @param mixed $stringValue Value to check + * @param mixed (string) $stringValue Value to check * * @return null|string */ @@ -38,7 +38,7 @@ class Trim /** * TRIMSPACES. * - * @param mixed $stringValue Value to check + * @param mixed (string) $stringValue Value to check * * @return null|string */ diff --git a/tests/data/Calculation/Statistical/BETADIST.php b/tests/data/Calculation/Statistical/BETADIST.php index 9fbecaaa..2046e189 100644 --- a/tests/data/Calculation/Statistical/BETADIST.php +++ b/tests/data/Calculation/Statistical/BETADIST.php @@ -25,12 +25,56 @@ return [ 0.685470581054, 2, 8, 10, 1, 3, ], + [ + 0.4059136, + 0.4, 4, 5, + ], + [ + '#VALUE!', + 'NAN', 8, 10, 1, 3, + ], [ '#VALUE!', 2, 'NAN', 10, 1, 3, ], [ + '#VALUE!', + 2, 8, 'NAN', 1, 3, + ], + [ + '#VALUE!', + 2, 8, 10, 'NAN', 3, + ], + [ + '#VALUE!', + 2, 8, 10, 1, 'NAN', + ], + 'alpha < 0' => [ '#NUM!', 2, -8, 10, 1, 3, ], + 'alpha = 0' => [ + '#NUM!', + 2, 0, 10, 1, 3, + ], + 'beta < 0' => [ + '#NUM!', + 2, 8, -10, 1, 3, + ], + 'beta = 0' => [ + '#NUM!', + 2, 8, 0, 1, 3, + ], + 'value < Min' => [ + '#NUM!', + 0.5, 8, 10, 1, 3, + ], + 'value > Max' => [ + '#NUM!', + 3.5, 8, 10, 1, 3, + ], + 'Min = Max' => [ + '#NUM!', + 2, 8, 10, 2, 2, + ], ]; diff --git a/tests/data/Calculation/Statistical/BETAINV.php b/tests/data/Calculation/Statistical/BETAINV.php index 5afe14cb..4d8cb5bd 100644 --- a/tests/data/Calculation/Statistical/BETAINV.php +++ b/tests/data/Calculation/Statistical/BETAINV.php @@ -25,12 +25,56 @@ return [ 0.303225844664, 0.2, 4, 5, 0, 1, ], + [ + '#VALUE!', + 'NAN', 4, 5, 0, 1, + ], [ '#VALUE!', 0.2, 'NAN', 5, 0, 1, ], [ + '#VALUE!', + 0.2, 4, 'NAN', 0, 1, + ], + [ + '#VALUE!', + 0.2, 4, 5, 'NAN', 1, + ], + [ + '#VALUE!', + 0.2, 4, 5, 0, 'NAN', + ], + 'alpha < 0' => [ '#NUM!', 0.2, -4, 5, 0, 1, ], + 'alpha = 0' => [ + '#NUM!', + 0.2, 0, 5, 0, 1, + ], + 'beta < 0' => [ + '#NUM!', + 0.2, 4, -5, 0, 1, + ], + 'beta = 0' => [ + '#NUM!', + 0.2, 4, 0, 0, 1, + ], + 'Probability < 0' => [ + '#NUM!', + -0.5, 4, 5, 1, 3, + ], + 'Probability = 0' => [ + '#NUM!', + 0.0, 4, 5, 1, 3, + ], + 'Probability > 1' => [ + '#NUM!', + 1.5, 4, 5, 1, 3, + ], + 'Min = Max' => [ + '#NUM!', + 1, 4, 5, 1, 1, + ], ]; diff --git a/tests/data/Calculation/Statistical/CHIDIST.php b/tests/data/Calculation/Statistical/CHIDIST.php index 5cfdc664..24ddab9f 100644 --- a/tests/data/Calculation/Statistical/CHIDIST.php +++ b/tests/data/Calculation/Statistical/CHIDIST.php @@ -35,14 +35,18 @@ return [ ], [ '#VALUE!', - 'NAN', 3, + 'NaN', 3, ], [ - '#NUM!', - 8, 0, + '#VALUE!', + 8, 'NaN', ], - [ + 'Value < 0' => [ '#NUM!', -8, 3, ], + 'Degrees < 1' => [ + '#NUM!', + 8, 0, + ], ]; diff --git a/tests/data/Calculation/Statistical/CHIINV.php b/tests/data/Calculation/Statistical/CHIINV.php index 2384cda6..f931a780 100644 --- a/tests/data/Calculation/Statistical/CHIINV.php +++ b/tests/data/Calculation/Statistical/CHIINV.php @@ -35,6 +35,22 @@ return [ ], [ '#VALUE!', - 0.25, 'NAN', + 'NaN', 3, + ], + [ + '#VALUE!', + 0.25, 'NaN', + ], + 'Probability < 0' => [ + '#NUM!', + -0.1, 3, + ], + 'Probability > 1' => [ + '#NUM!', + 1.1, 3, + ], + 'Freedom > 1' => [ + '#NUM!', + 0.1, 0.5, ], ]; diff --git a/tests/data/Calculation/Statistical/FISHER.php b/tests/data/Calculation/Statistical/FISHER.php index 12909012..faf6442e 100644 --- a/tests/data/Calculation/Statistical/FISHER.php +++ b/tests/data/Calculation/Statistical/FISHER.php @@ -13,12 +13,20 @@ return [ 1.098612288668, 0.8, ], + [ + 0.972955074528, + 0.75, + ], [ '#VALUE!', 'NAN', ], [ '#NUM!', - -2, + -1.5, + ], + [ + '#NUM!', + 1.5, ], ]; diff --git a/tests/data/Calculation/Statistical/FISHERINV.php b/tests/data/Calculation/Statistical/FISHERINV.php index b79fd4f8..d23472b2 100644 --- a/tests/data/Calculation/Statistical/FISHERINV.php +++ b/tests/data/Calculation/Statistical/FISHERINV.php @@ -17,6 +17,10 @@ return [ 0.992631520201, 2.8, ], + [ + 0.7499999990254, + 0.9729550723, + ], [ '#VALUE!', 'NAN', diff --git a/tests/data/Calculation/Statistical/GAMMA.php b/tests/data/Calculation/Statistical/GAMMA.php index 760b429d..24b83f04 100644 --- a/tests/data/Calculation/Statistical/GAMMA.php +++ b/tests/data/Calculation/Statistical/GAMMA.php @@ -6,8 +6,10 @@ return [ [9.513507698669, 0.1], [1.0, 1.0], [0.886226925453, 1.5], + [1.3293403881791, 2.5], [17.837861981813, 4.8], [52.342777784553, 5.5], - ['#NUM!', -1], + 'Zero value' => ['#NUM!', 0.0], + 'Negative integer value' => ['#NUM!', -1], ['#VALUE!', 'NAN'], ]; diff --git a/tests/data/Calculation/Statistical/GAMMADIST.php b/tests/data/Calculation/Statistical/GAMMADIST.php index e79b3869..2a1bfa14 100644 --- a/tests/data/Calculation/Statistical/GAMMADIST.php +++ b/tests/data/Calculation/Statistical/GAMMADIST.php @@ -17,12 +17,44 @@ return [ 0.576809918873, 6, 3, 2, true, ], + 'Boolean as numeric' => [ + 0.576809918873, + 6, 3, 2, 1, + ], + [ + '#VALUE!', + 'NAN', 3, 2, true, + ], [ '#VALUE!', 6, 'NAN', 2, true, ], [ + '#VALUE!', + 6, 3, 'NAN', true, + ], + [ + '#VALUE!', + 6, 3, 2, 'NAN', + ], + 'Value < 0' => [ '#NUM!', -6, 3, 2, true, ], + 'A < 0' => [ + '#NUM!', + 6, -3, 2, true, + ], + 'A = 0' => [ + '#NUM!', + 6, 0, 2, true, + ], + 'B < 0' => [ + '#NUM!', + 6, 3, -2, true, + ], + 'B = 0' => [ + '#NUM!', + 6, 3, 0, true, + ], ]; diff --git a/tests/data/Calculation/Statistical/GAMMAINV.php b/tests/data/Calculation/Statistical/GAMMAINV.php index 3b3604b4..c35c4219 100644 --- a/tests/data/Calculation/Statistical/GAMMAINV.php +++ b/tests/data/Calculation/Statistical/GAMMAINV.php @@ -11,10 +11,38 @@ return [ ], [ '#VALUE!', - 'NAN', 3, 2, + 'NaN', 3, 2, ], [ + '#VALUE!', + 0.5, 'NaN', 2, + ], + [ + '#VALUE!', + 0.5, 3, 'NaN', + ], + 'Probability < 0' => [ '#NUM!', -0.5, 3, 2, ], + 'Probability > 1' => [ + '#NUM!', + 1.5, 3, 2, + ], + 'Alpha < 0' => [ + '#NUM!', + 0.5, -3, 2, + ], + 'Alpha = 0' => [ + '#NUM!', + 0.5, 0, 2, + ], + 'Beta < 0' => [ + '#NUM!', + 0.5, 3, -2, + ], + 'Beta = 0' => [ + '#NUM!', + 0.5, 3, 0, + ], ]; diff --git a/tests/data/Calculation/Statistical/GAMMALN.php b/tests/data/Calculation/Statistical/GAMMALN.php index a415f559..7b43eea8 100644 --- a/tests/data/Calculation/Statistical/GAMMALN.php +++ b/tests/data/Calculation/Statistical/GAMMALN.php @@ -13,8 +13,12 @@ return [ '#VALUE!', 'NAN', ], - [ + 'Value < 0' => [ '#NUM!', -4.5, ], + 'Value = 0' => [ + '#NUM!', + 0.0, + ], ]; diff --git a/tests/data/Calculation/TextData/FIND.php b/tests/data/Calculation/TextData/FIND.php index 0a583456..04d3276d 100644 --- a/tests/data/Calculation/TextData/FIND.php +++ b/tests/data/Calculation/TextData/FIND.php @@ -101,4 +101,9 @@ return [ 'Mark Baker', 8, ], + 'Boolean Needle' => [ + '#VALUE!', + true, + 'Mark Baker', + ], ]; diff --git a/tests/data/Calculation/TextData/SEARCH.php b/tests/data/Calculation/TextData/SEARCH.php index 579830f6..fa970bec 100644 --- a/tests/data/Calculation/TextData/SEARCH.php +++ b/tests/data/Calculation/TextData/SEARCH.php @@ -94,4 +94,9 @@ return [ 'BITE', 'BIT', ], + 'Boolean Needle' => [ + '#VALUE!', + true, + 'Mark Baker', + ], ]; From c380b25d3cf7843765462cdac3ae163b34a935a8 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 26 Mar 2021 09:08:23 +0100 Subject: [PATCH 62/89] Extract Poisson distribution into its own class (#1953) --- .../Calculation/Calculation.php | 4 +- .../Calculation/DateTimeExcel/Days360.php | 2 +- .../Calculation/Engineering/Complex.php | 6 +- src/PhpSpreadsheet/Calculation/Financial.php | 42 +++++----- .../Calculation/Financial/Depreciation.php | 14 ++-- src/PhpSpreadsheet/Calculation/Functions.php | 4 +- src/PhpSpreadsheet/Calculation/LookupRef.php | 4 +- .../Calculation/LookupRef/Matrix.php | 2 +- .../Calculation/Statistical.php | 77 ++++++++----------- .../Statistical/Distributions/Poisson.php | 55 +++++++++++++ .../data/Calculation/Statistical/POISSON.php | 16 +++- 11 files changed, 138 insertions(+), 88 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 80a6fbcc..792402ce 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1963,12 +1963,12 @@ class Calculation ], 'POISSON' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'POISSON'], + 'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'], 'argumentCount' => '3', ], 'POISSON.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'POISSON'], + 'functionCall' => [Statistical\Distributions\Poisson::class, 'distribution'], 'argumentCount' => '3', ], 'POWER' => [ diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php index 068ea2bc..b90bc367 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php @@ -22,7 +22,7 @@ class Days360 * 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 mixed (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 diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Complex.php b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php index f6429cbd..7dd5ff95 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Complex.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php @@ -16,9 +16,9 @@ class Complex * Excel Function: * COMPLEX(realNumber,imaginary[,suffix]) * - * @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 mixed (float) $realNumber the real coefficient of the complex number + * @param mixed (float) $imaginary the imaginary coefficient of the complex number + * @param mixed (string) $suffix The suffix for the imaginary component of the complex number. * If omitted, the suffix is assumed to be "i". * * @return string diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 2b54e1cd..084562f8 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -42,15 +42,15 @@ class Financial * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue date * when the security is traded to the buyer. - * @param float $rate the security's annual coupon rate - * @param float $par The security's par value. + * @param mixed (float) $rate the security's annual coupon rate + * @param mixed (float) $par The security's par value. * If you omit par, ACCRINT uses $1,000. - * @param int $frequency the number of coupon payments per year. + * @param mixed (int) $frequency the number of coupon payments per year. * Valid frequency values are: * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -98,10 +98,10 @@ class Financial * * @param mixed $issue The security's issue date * @param mixed $settlement The security's settlement (or maturity) date - * @param float $rate The security's annual coupon rate - * @param float $par The security's par value. + * @param mixed (float) $rate The security's annual coupon rate + * @param mixed (float) $par The security's par value. * If you omit par, ACCRINT uses $1,000. - * @param int $basis The type of day count to use. + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -890,11 +890,11 @@ class Financial * Excel Function: * IRR(values[,guess]) * - * @param float[] $values An array or a reference to cells that contain numbers for which you want + * @param mixed (float[]) $values An array or a reference to cells that contain numbers for which you want * to calculate the internal rate of return. * Values must contain at least one positive value and one negative value to * calculate the internal rate of return. - * @param float $guess A number that you guess is close to the result of IRR + * @param mixed (float) $guess A number that you guess is close to the result of IRR * * @return float|string */ @@ -1000,11 +1000,11 @@ class Financial * Excel Function: * MIRR(values,finance_rate, reinvestment_rate) * - * @param float[] $values An array or a reference to cells that contain a series of payments and + * @param mixed (float[]) $values An array or a reference to cells that contain a series of payments and * income occurring at regular intervals. * Payments are negative value, income is positive values. - * @param float $finance_rate The interest rate you pay on the money used in the cash flows - * @param float $reinvestment_rate The interest rate you receive on the cash flows as you reinvest them + * @param mixed (float) $finance_rate The interest rate you pay on the money used in the cash flows + * @param mixed (float) $reinvestment_rate The interest rate you receive on the cash flows as you reinvest them * * @return float|string Result, or a string containing an error */ @@ -1371,20 +1371,20 @@ class Financial * Excel Function: * RATE(nper,pmt,pv[,fv[,type[,guess]]]) * - * @param float $nper The total number of payment periods in an annuity - * @param float $pmt The payment made each period and cannot change over the life + * @param mixed (float) $nper The total number of payment periods in an annuity + * @param mixed (float) $pmt The payment made each period and cannot change over the life * of the annuity. * Typically, pmt includes principal and interest but no other * fees or taxes. - * @param float $pv The present value - the total amount that a series of future + * @param mixed (float) $pv The present value - the total amount that a series of future * payments is worth now - * @param float $fv The future value, or a cash balance you want to attain after + * @param mixed (float) $fv The future value, or a cash balance you want to attain after * the last payment is made. If fv is omitted, it is assumed * to be 0 (the future value of a loan, for example, is 0). - * @param int $type A number 0 or 1 and indicates when payments are due: + * @param mixed (int) $type A number 0 or 1 and indicates when payments are due: * 0 or omitted At the end of the period. * 1 At the beginning of the period. - * @param float $guess Your guess for what the rate will be. + * @param mixed (float) $guess Your guess for what the rate will be. * If you omit guess, it is assumed to be 10 percent. * * @return float|string @@ -1443,9 +1443,9 @@ class Financial * The security settlement date is the date after the issue date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. - * @param int $investment The amount invested in the security - * @param int $discount The security's discount rate - * @param int $basis The type of day count to use. + * @param mixed (int) $investment The amount invested in the security + * @param mixed (int) $discount The security's discount rate + * @param mixed (int) $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 diff --git a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php index 173e29bb..8770242f 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php @@ -137,9 +137,9 @@ class Depreciation * * Returns the straight-line depreciation of an asset for one period * - * @param mixed $cost Initial cost of the asset - * @param mixed $salvage Value at the end of the depreciation - * @param mixed $life Number of periods over which the asset is depreciated + * @param mixed (float) $cost Initial cost of the asset + * @param mixed (float) $salvage Value at the end of the depreciation + * @param mixed (float) $life Number of periods over which the asset is depreciated * * @return float|string Result, or a string containing an error */ @@ -169,10 +169,10 @@ class Depreciation * * Returns the sum-of-years' digits depreciation of an asset for a specified period. * - * @param mixed $cost Initial cost of the asset - * @param mixed $salvage Value at the end of the depreciation - * @param mixed $life Number of periods over which the asset is depreciated - * @param mixed $period Period + * @param mixed (float) $cost Initial cost of the asset + * @param mixed (float) $salvage Value at the end of the depreciation + * @param mixed (float) $life Number of periods over which the asset is depreciated + * @param mixed (float) $period Period * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 022e6be5..6ad387e8 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -576,7 +576,7 @@ class Functions /** * Convert a multi-dimensional array to a simple 1-dimensional array. * - * @param array $array Array to be flattened + * @param mixed (array) $array Array to be flattened * * @return array Flattened array */ @@ -609,7 +609,7 @@ class Functions /** * Convert a multi-dimensional array to a simple 1-dimensional array, but retain an element of indexing. * - * @param array $array Array to be flattened + * @param mixed (array) $array Array to be flattened * * @return array Flattened array */ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 17115a06..4a1bcb06 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -148,8 +148,8 @@ class LookupRef * Excel Function: * =HYPERLINK(linkURL,displayName) * - * @param string $linkURL Value to check, is also the value returned when no error - * @param string $displayName Value to return when testValue is an error condition + * @param mixed (string) $linkURL Value to check, is also the value returned when no error + * @param mixed (string) $displayName Value to return when testValue is an error condition * @param Cell $pCell The cell to set the hyperlink in * * @return mixed The value of $displayName (or $linkURL if $displayName was blank) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index 8859a287..59af4258 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -9,7 +9,7 @@ class Matrix /** * TRANSPOSE. * - * @param array $matrixData A matrix of values + * @param mixed (array) $matrixData A matrix of values * * @return array */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index c8e084b5..ca160c36 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -251,10 +251,10 @@ class Statistical * experiment. For example, BINOMDIST can calculate the probability that two of the next three * babies born are male. * - * @param float $value Number of successes in trials - * @param float $trials Number of trials - * @param float $probability Probability of success on each trial - * @param bool $cumulative + * @param mixed (float) $value Number of successes in trials + * @param mixed (float) $trials Number of trials + * @param mixed (float) $probability Probability of success on each trial + * @param mixed (bool) $cumulative * * @return float|string */ @@ -1502,9 +1502,9 @@ class Statistical * distribution, except that the number of successes is fixed, and the number of trials is * variable. Like the binomial, trials are assumed to be independent. * - * @param float $failures Number of Failures - * @param float $successes Threshold number of Successes - * @param float $probability Probability of success on each trial + * @param mixed (float) $failures Number of Failures + * @param mixed (float) $successes Threshold number of Successes + * @param mixed (float) $probability Probability of success on each trial * * @return float|string The result, or a string containing an error */ @@ -1539,10 +1539,10 @@ class Statistical * function has a very wide range of applications in statistics, including hypothesis * testing. * - * @param float $value - * @param float $mean Mean Value - * @param float $stdDev Standard Deviation - * @param bool $cumulative + * @param mixed (float) $value + * @param mixed (float) $mean Mean Value + * @param mixed (float) $stdDev Standard Deviation + * @param mixed (bool) $cumulative * * @return float|string The result, or a string containing an error */ @@ -1573,9 +1573,9 @@ class Statistical * * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. * - * @param float $probability - * @param float $mean Mean Value - * @param float $stdDev Standard Deviation + * @param mixed (float) $probability + * @param mixed (float) $mean Mean Value + * @param mixed (float) $stdDev Standard Deviation * * @return float|string The result, or a string containing an error */ @@ -1606,7 +1606,7 @@ class Statistical * 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 float $value + * @param mixed (float) $value * * @return float|string The result, or a string containing an error */ @@ -1627,8 +1627,8 @@ class Statistical * 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 float $value - * @param bool $cumulative + * @param mixed (float) $value + * @param mixed (bool) $cumulative * * @return float|string The result, or a string containing an error */ @@ -1648,7 +1648,7 @@ class Statistical * * Returns the inverse of the standard normal cumulative distribution * - * @param float $value + * @param mixed (float) $value * * @return float|string The result, or a string containing an error */ @@ -1714,9 +1714,9 @@ class Statistical * rather than floored (as MS Excel), so value 3 for a value set of 1, 2, 3, 4 will return * 0.667 rather than 0.666 * - * @param float[] $valueSet An array of, or a reference to, a list of numbers - * @param int $value the number whose rank you want to find - * @param int $significance the number of significant digits for the returned percentage value + * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers + * @param mixed (int) $value the number whose rank you want to find + * @param mixed (int) $significance the number of significant digits for the returned percentage value * * @return float|string (string if result is an error) */ @@ -1787,37 +1787,20 @@ class Statistical * is predicting the number of events over a specific time, such as the number of * cars arriving at a toll plaza in 1 minute. * - * @param float $value - * @param float $mean Mean Value - * @param bool $cumulative + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Poisson::distribution() + * Use the distribution() method in the Statistical\Distributions\Poisson class instead + * + * @param mixed (float) $value + * @param mixed (float) $mean Mean Value + * @param mixed (bool) $cumulative * * @return float|string The result, or a string containing an error */ public static function POISSON($value, $mean, $cumulative) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - - if ((is_numeric($value)) && (is_numeric($mean))) { - if (($value < 0) || ($mean <= 0)) { - return Functions::NAN(); - } - if ((is_numeric($cumulative)) || (is_bool($cumulative))) { - if ($cumulative) { - $summer = 0; - $floor = floor($value); - for ($i = 0; $i <= $floor; ++$i) { - $summer += $mean ** $i / MathTrig::FACT($i); - } - - return exp(0 - $mean) * $summer; - } - - return (exp(0 - $mean) * $mean ** $value) / MathTrig::FACT($value); - } - } - - return Functions::VALUE(); + return Statistical\Distributions\Poisson::distribution($value, $mean, $cumulative); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php new file mode 100644 index 00000000..1ba7adca --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php @@ -0,0 +1,55 @@ +getMessage(); + } + + if (($value < 0) || ($mean < 0)) { + return Functions::NAN(); + } + + if ($cumulative) { + $summer = 0; + $floor = floor($value); + for ($i = 0; $i <= $floor; ++$i) { + $summer += $mean ** $i / MathTrig::FACT($i); + } + + return exp(0 - $mean) * $summer; + } + + return (exp(0 - $mean) * $mean ** $value) / MathTrig::FACT($value); + } +} diff --git a/tests/data/Calculation/Statistical/POISSON.php b/tests/data/Calculation/Statistical/POISSON.php index 11a82cab..7c839ac7 100644 --- a/tests/data/Calculation/Statistical/POISSON.php +++ b/tests/data/Calculation/Statistical/POISSON.php @@ -18,11 +18,23 @@ return [ 35, 40, true, ], [ - '#NUM!', - 35, -40, true, + '#VALUE!', + 'Nan', 40, true, ], [ '#VALUE!', 35, 'Nan', true, ], + [ + '#VALUE!', + 35, 40, 'Nan', + ], + 'Value < 0' => [ + '#NUM!', + -35, 40, true, + ], + 'Mean < 0' => [ + '#NUM!', + 35, -40, true, + ], ]; From 9239b3deca880a0ab66692bd6011b7b89f6274dc Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 26 Mar 2021 01:35:30 -0700 Subject: [PATCH 63/89] Continue MathTrig Breakup - Problem Children (#1954) Continuing the process of breaking MathTrip.php up into smaller classes. This round takes care of all functions which might be an impediment to installing due to either uncovered code or "complexity": - BASE - FACT - LCM - MDETERM, MINVERSE, MMULT - MULTINOMIAL - PRODUCT - QUOTIENT - SERIESSUM - SUM - SUMPRODUCT MathTrig and the members in directory MathTrig are now 100% covered. Many tests have been added, and some edge-case bugs are corrected. Some cases where PhpSpreadsheet had rejected numeric values stored as strings have been changed to accept them whenever Excel does; there had been no tests for that condition. Boolean arguments are now accepted as arguments wherever Excel accpets them. Taking a cue from what has been done in Engineering, the parameter validation now happens in a routine which issues Exceptions for invalid values; this simplifies the code in the functions themselves. Thank you for doing that; I did not foresee how useful that was when I first looked at it. Consistent with earlier changes of this nature, the versions in the MathTrig class remain, with a doc block indicating deprecation, and a stub call to the new routines. All tests except for MINVERSE and MMULT are now handled in the context of a spreadsheet rather than a direct call to the calculation function which implements it. PhpSpreadsheet would need to handle dynamic arrays in order to test MINVERSE and MMULT in a spreadsheet context. Implementing that looks like it might be *very* challenging. It is not something I plan to look at, at least not in the near future. One parsing problem turned up in the test conversion. It is in one of the SUMIF tests. It takes me to an area in Calculation where the comment says "I don't even want to know what you did to get here". It did not show up in the previous incarnation because, by using a direct call, the previous test managed to bypass the parsing. I have confirmed that this problem shows up in earlier releases of PhpSpreadsheet, so the changes in this PR did not cause it - they merely exposed it. I have left the test intact, but marked it "incomplete" for documentation purposes. I have not been able to get a handle on what's going wrong yet. I will probably open an issue on it if I can't resolve it soon. However, the test in question isn't a "real world" issue, and the error wasn't caused by this change, so I see no reason to delay this pending a resolution of the problem. SUM had an idiosyncratic moment of its own. It had been ignoring non-numeric values, but Excel returns VALUE in that situation. So I changed it and wrote some new tests, which worked, but ... SUMIF uses several levels of indirection to get to SUM, and SUMIF *does* ignore non-numeric values, so a SUMIF test broke. SUM is a really simple function; the most practical approach seemed to be to clone it, with the string-accepting version being used by the Legacy version (which is called by SUMIF), and the non-string-accepting version being used in the Calculation Function table. That seems far easier and more practical than, for instance, adding a boolean parameter to the variable parameter list. As a follow-up, I will change SUMIF to explicitly call the appropriate new version, but I did not want to add that to this already large change. SUM again - although it was fully covered beforehand, there was not a specific test member for it. There is now. FACT had been coded to fail Gnumeric requests where the numeric argument has a decimal portion. However, Gnumeric does accept such an argument, and, unlike Excel and ODS, does not truncate it, but returns the result of a Gamma function call instead. This has been corrected. When LCM included arguments which contained both 0 and a negative number, it returned 0 or NUM, whichever it found first. It is changed to always return NUM in that circumstance, as Excel does. QUOTIENT had been documented as taking a variadic list of arguments. In fact, it takes exactly 2 - numerator and denominator - and the docblock and signature is fixed, even in the deprecated version. The SERIESSUM docbock and signature are more accurate, even in the deprecated version. It is changed to ignore nulls, as Excel does, rather than return VALUE, and is one of the routines which previously rejected numbers in string form. SUBTOTAL tests had used mocking for some reason. These are replaced with normal tests. And SUBTOTAL had a big surprise in store. That part of it which deals with hidden cells cares only whether the row is hidden, and doesn't care about the column's visibility. I struggled with whether it should be SubTotal or Subtotal. I think the latter is correct, so that's how I proceeded. I don't think there are likely to be any other capitalization controversies. --- .../Calculation/Calculation.php | 24 +- src/PhpSpreadsheet/Calculation/Database.php | 4 +- .../Calculation/Database/DProduct.php | 2 +- .../Calculation/Database/DSum.php | 2 +- src/PhpSpreadsheet/Calculation/MathTrig.php | 494 ++---------------- .../Calculation/MathTrig/Base.php | 49 ++ .../Calculation/MathTrig/Fact.php | 47 ++ .../Calculation/MathTrig/Helpers.php | 28 + .../Calculation/MathTrig/Lcm.php | 95 ++++ .../Calculation/MathTrig/MatrixFunctions.php | 114 ++++ .../Calculation/MathTrig/Multinomial.php | 41 ++ .../Calculation/MathTrig/Product.php | 47 ++ .../Calculation/MathTrig/Quotient.php | 35 ++ .../Calculation/MathTrig/SeriesSum.php | 46 ++ .../Calculation/MathTrig/Subtotal.php | 99 ++++ .../Calculation/MathTrig/Sum.php | 68 +++ .../Calculation/MathTrig/SumProduct.php | 49 ++ .../Calculation/Statistical/Conditional.php | 2 +- .../Functions/MathTrig/AcosTest.php | 13 +- .../Functions/MathTrig/AcoshTest.php | 13 +- .../Functions/MathTrig/AcotTest.php | 13 +- .../Functions/MathTrig/AcothTest.php | 13 +- .../Functions/MathTrig/AllSetupTeardown.php | 52 ++ .../Functions/MathTrig/AsinTest.php | 13 +- .../Functions/MathTrig/AsinhTest.php | 13 +- .../Functions/MathTrig/Atan2Test.php | 13 +- .../Functions/MathTrig/AtanTest.php | 13 +- .../Functions/MathTrig/AtanhTest.php | 13 +- .../Functions/MathTrig/BaseTest.php | 38 +- .../Functions/MathTrig/CeilingMathTest.php | 13 +- .../Functions/MathTrig/CeilingPreciseTest.php | 13 +- .../Functions/MathTrig/CeilingTest.php | 45 +- .../Functions/MathTrig/CosTest.php | 13 +- .../Functions/MathTrig/CoshTest.php | 13 +- .../Functions/MathTrig/CotTest.php | 13 +- .../Functions/MathTrig/CothTest.php | 13 +- .../Functions/MathTrig/CscTest.php | 13 +- .../Functions/MathTrig/CschTest.php | 13 +- .../Functions/MathTrig/EvenTest.php | 13 +- .../Functions/MathTrig/FactTest.php | 57 +- .../Functions/MathTrig/FloorMathTest.php | 13 +- .../Functions/MathTrig/FloorPreciseTest.php | 13 +- .../Functions/MathTrig/FloorTest.php | 43 +- .../Functions/MathTrig/IntTest.php | 13 +- .../Functions/MathTrig/LcmTest.php | 20 +- .../Functions/MathTrig/MInverseTest.php | 17 +- .../Functions/MathTrig/MMultTest.php | 17 +- .../Functions/MathTrig/MRoundTest.php | 13 +- .../Functions/MathTrig/MdeTermTest.php | 26 +- .../Functions/MathTrig/MovedFunctionsTest.php | 21 + .../Functions/MathTrig/MultinomialTest.php | 25 +- .../Functions/MathTrig/OddTest.php | 13 +- .../Functions/MathTrig/ProductTest.php | 20 +- .../Functions/MathTrig/QuotientTest.php | 34 +- .../Functions/MathTrig/RomanTest.php | 20 +- .../Functions/MathTrig/RoundDownTest.php | 13 +- .../Functions/MathTrig/RoundTest.php | 13 +- .../Functions/MathTrig/RoundUpTest.php | 13 +- .../Functions/MathTrig/SecTest.php | 13 +- .../Functions/MathTrig/SechTest.php | 13 +- .../Functions/MathTrig/SeriesSumTest.php | 35 +- .../Functions/MathTrig/SignTest.php | 13 +- .../Functions/MathTrig/SinTest.php | 13 +- .../Functions/MathTrig/SinhTest.php | 13 +- .../Functions/MathTrig/SubTotalTest.php | 258 ++++----- .../Functions/MathTrig/SumIfTest.php | 35 +- .../Functions/MathTrig/SumProductTest.php | 28 +- .../Functions/MathTrig/SumTest.php | 29 + .../Functions/MathTrig/TanTest.php | 13 +- .../Functions/MathTrig/TanhTest.php | 13 +- .../Functions/MathTrig/TruncTest.php | 13 +- tests/data/Calculation/MathTrig/BASE.php | 6 + tests/data/Calculation/MathTrig/FACT.php | 4 +- .../Calculation/MathTrig/FACTGNUMERIC.php | 44 ++ tests/data/Calculation/MathTrig/LCM.php | 5 + tests/data/Calculation/MathTrig/MDETERM.php | 34 +- tests/data/Calculation/MathTrig/MINVERSE.php | 28 + tests/data/Calculation/MathTrig/MMULT.php | 22 + .../data/Calculation/MathTrig/MULTINOMIAL.php | 6 + tests/data/Calculation/MathTrig/PRODUCT.php | 2 + tests/data/Calculation/MathTrig/QUOTIENT.php | 8 + tests/data/Calculation/MathTrig/SERIESSUM.php | 12 + tests/data/Calculation/MathTrig/SUBTOTAL.php | 85 +-- .../Calculation/MathTrig/SUBTOTALHIDDEN.php | 107 +--- .../Calculation/MathTrig/SUBTOTALNESTED.php | 18 - tests/data/Calculation/MathTrig/SUM.php | 8 + tests/data/Calculation/MathTrig/SUMIF.php | 30 +- .../data/Calculation/MathTrig/SUMPRODUCT.php | 10 + 88 files changed, 1559 insertions(+), 1378 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Base.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Fact.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Multinomial.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Product.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Quotient.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Sum.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/SumProduct.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php create mode 100644 tests/data/Calculation/MathTrig/FACTGNUMERIC.php delete mode 100644 tests/data/Calculation/MathTrig/SUBTOTALNESTED.php create mode 100644 tests/data/Calculation/MathTrig/SUM.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 792402ce..3cce499f 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -358,7 +358,7 @@ class Calculation ], 'BASE' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'BASE'], + 'functionCall' => [MathTrig\Base::class, 'funcBase'], 'argumentCount' => '2,3', ], 'BESSELI' => [ @@ -990,7 +990,7 @@ class Calculation ], 'FACT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'FACT'], + 'functionCall' => [MathTrig\Fact::class, 'funcFact'], 'argumentCount' => '1', ], 'FACTDOUBLE' => [ @@ -1536,7 +1536,7 @@ class Calculation ], 'LCM' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'LCM'], + 'functionCall' => [MathTrig\Lcm::class, 'funcLcm'], 'argumentCount' => '1+', ], 'LEFT' => [ @@ -1636,7 +1636,7 @@ class Calculation ], 'MDETERM' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'MDETERM'], + 'functionCall' => [MathTrig\MatrixFunctions::class, 'funcMDeterm'], 'argumentCount' => '1', ], 'MDURATION' => [ @@ -1686,7 +1686,7 @@ class Calculation ], 'MINVERSE' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'MINVERSE'], + 'functionCall' => [MathTrig\MatrixFunctions::class, 'funcMinverse'], 'argumentCount' => '1', ], 'MIRR' => [ @@ -1696,7 +1696,7 @@ class Calculation ], 'MMULT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'MMULT'], + 'functionCall' => [MathTrig\MatrixFunctions::class, 'funcMMult'], 'argumentCount' => '2', ], 'MOD' => [ @@ -1731,7 +1731,7 @@ class Calculation ], 'MULTINOMIAL' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'MULTINOMIAL'], + 'functionCall' => [MathTrig\Multinomial::class, 'funcMultinomial'], 'argumentCount' => '1+', ], 'MUNIT' => [ @@ -2003,7 +2003,7 @@ class Calculation ], 'PRODUCT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'PRODUCT'], + 'functionCall' => [MathTrig\Product::class, 'funcProduct'], 'argumentCount' => '1+', ], 'PROPER' => [ @@ -2033,7 +2033,7 @@ class Calculation ], 'QUOTIENT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'QUOTIENT'], + 'functionCall' => [MathTrig\Quotient::class, 'funcQuotient'], 'argumentCount' => '2', ], 'RADIANS' => [ @@ -2305,13 +2305,13 @@ class Calculation ], 'SUBTOTAL' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUBTOTAL'], + 'functionCall' => [MathTrig\Subtotal::class, 'funcSubtotal'], 'argumentCount' => '2+', 'passCellReference' => true, ], 'SUM' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUM'], + 'functionCall' => [MathTrig\Sum::class, 'funcSumNoStrings'], 'argumentCount' => '1+', ], 'SUMIF' => [ @@ -2326,7 +2326,7 @@ class Calculation ], 'SUMPRODUCT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUMPRODUCT'], + 'functionCall' => [MathTrig\SumProduct::class, 'funcSumProduct'], 'argumentCount' => '1+', ], 'SUMSQ' => [ diff --git a/src/PhpSpreadsheet/Calculation/Database.php b/src/PhpSpreadsheet/Calculation/Database.php index 76431979..a4c4d7d2 100644 --- a/src/PhpSpreadsheet/Calculation/Database.php +++ b/src/PhpSpreadsheet/Calculation/Database.php @@ -245,7 +245,7 @@ class Database * the column label in which you specify a condition for the * column. * - * @return float + * @return float|string */ public static function DPRODUCT($database, $field, $criteria) { @@ -349,7 +349,7 @@ class Database * the column label in which you specify a condition for the * column. * - * @return float + * @return float|string */ public static function DSUM($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DProduct.php b/src/PhpSpreadsheet/Calculation/Database/DProduct.php index a3b245f3..107c69c0 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DProduct.php +++ b/src/PhpSpreadsheet/Calculation/Database/DProduct.php @@ -29,7 +29,7 @@ class DProduct extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float + * @return float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DSum.php b/src/PhpSpreadsheet/Calculation/Database/DSum.php index b2fa464a..473bacd1 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DSum.php +++ b/src/PhpSpreadsheet/Calculation/Database/DSum.php @@ -29,7 +29,7 @@ class DSum extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float + * @return float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index 94850906..1dbc2854 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -3,37 +3,9 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; use Exception; -use Matrix\Exception as MatrixException; -use Matrix\Matrix; class MathTrig { - // - // Private method to return an array of the factors of the input value - // - private static function factors($value) - { - $startVal = floor(sqrt($value)); - - $factorArray = []; - for ($i = $startVal; $i > 1; --$i) { - if (($value % $i) == 0) { - $factorArray = array_merge($factorArray, self::factors($value / $i)); - $factorArray = array_merge($factorArray, self::factors($i)); - if ($i <= sqrt($value)) { - break; - } - } - } - if (!empty($factorArray)) { - rsort($factorArray); - - return $factorArray; - } - - return [(int) $value]; - } - private static function strSplit(string $roman): array { $rslt = str_split($roman); @@ -153,6 +125,8 @@ class MathTrig * * Converts a number into a text representation with the given radix (base). * + * @Deprecated 2.0.0 Use the funcBase method in the MathTrig\Base class instead + * * Excel Function: * BASE(Number, Radix [Min_length]) * @@ -164,29 +138,7 @@ class MathTrig */ public static function BASE($number, $radix, $minLength = null) { - $number = Functions::flattenSingleValue($number); - $radix = Functions::flattenSingleValue($radix); - $minLength = Functions::flattenSingleValue($minLength); - - if (is_numeric($number) && is_numeric($radix) && ($minLength === null || is_numeric($minLength))) { - // Truncate to an integer - $number = (int) $number; - $radix = (int) $radix; - $minLength = (int) $minLength; - - if ($number < 0 || $number >= 2 ** 53 || $radix < 2 || $radix > 36) { - return Functions::NAN(); // Numeric range constraints - } - - $outcome = strtoupper((string) base_convert($number, 10, $radix)); - if ($minLength !== null) { - $outcome = str_pad($outcome, $minLength, '0', STR_PAD_LEFT); // String padding - } - - return $outcome; - } - - return Functions::VALUE(); + return MathTrig\Base::funcBase($number, $radix, $minLength); } /** @@ -240,7 +192,7 @@ class MathTrig return Functions::NAN(); } - return round(self::FACT($numObjs) / self::FACT($numObjs - $numInSet)) / self::FACT($numInSet); + return round(MathTrig\Fact::funcFact($numObjs) / MathTrig\Fact::funcFact($numObjs - $numInSet)) / MathTrig\Fact::funcFact($numInSet); } return Functions::VALUE(); @@ -285,6 +237,8 @@ class MathTrig * Returns the factorial of a number. * The factorial of a number is equal to 1*2*3*...* number. * + * @Deprecated 2.0.0 Use the funcFact method in the MathTrig\Fact class instead + * * Excel Function: * FACT(factVal) * @@ -294,29 +248,7 @@ class MathTrig */ public static function FACT($factVal) { - $factVal = Functions::flattenSingleValue($factVal); - - if (is_numeric($factVal)) { - if ($factVal < 0) { - return Functions::NAN(); - } - $factLoop = floor($factVal); - if ( - (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) && - ($factVal > $factLoop) - ) { - return Functions::NAN(); - } - - $factorial = 1; - while ($factLoop > 1) { - $factorial *= $factLoop--; - } - - return $factorial; - } - - return Functions::VALUE(); + return MathTrig\Fact::funcFact($factVal); } /** @@ -487,6 +419,8 @@ class MathTrig * of all integer arguments number1, number2, and so on. Use LCM to add fractions * with different denominators. * + * @Deprecated 2.0.0 Use the funcLcm method in the MathTrig\Lcm class instead + * * Excel Function: * LCM(number1[,number2[, ...]]) * @@ -496,39 +430,7 @@ class MathTrig */ public static function LCM(...$args) { - $returnValue = 1; - $allPoweredFactors = []; - // Loop through arguments - foreach (Functions::flattenArray($args) as $value) { - if (!is_numeric($value)) { - return Functions::VALUE(); - } - if ($value == 0) { - return 0; - } elseif ($value < 0) { - return Functions::NAN(); - } - $myFactors = self::factors(floor($value)); - $myCountedFactors = array_count_values($myFactors); - $myPoweredFactors = []; - foreach ($myCountedFactors as $myCountedFactor => $myCountedPower) { - $myPoweredFactors[$myCountedFactor] = $myCountedFactor ** $myCountedPower; - } - foreach ($myPoweredFactors as $myPoweredValue => $myPoweredFactor) { - if (isset($allPoweredFactors[$myPoweredValue])) { - if ($allPoweredFactors[$myPoweredValue] < $myPoweredFactor) { - $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; - } - } else { - $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; - } - } - } - foreach ($allPoweredFactors as $allPoweredFactor) { - $returnValue *= (int) $allPoweredFactor; - } - - return $returnValue; + return MathTrig\Lcm::funcLcm(...$args); } /** @@ -564,6 +466,8 @@ class MathTrig * * Returns the matrix determinant of an array. * + * @Deprecated 2.0.0 Use the funcMDeterm method in the MathTrig\MatrixFuncs class instead + * * Excel Function: * MDETERM(array) * @@ -573,40 +477,7 @@ class MathTrig */ public static function MDETERM($matrixValues) { - $matrixData = []; - if (!is_array($matrixValues)) { - $matrixValues = [[$matrixValues]]; - } - - $row = $maxColumn = 0; - foreach ($matrixValues as $matrixRow) { - if (!is_array($matrixRow)) { - $matrixRow = [$matrixRow]; - } - $column = 0; - foreach ($matrixRow as $matrixCell) { - if ((is_string($matrixCell)) || ($matrixCell === null)) { - return Functions::VALUE(); - } - $matrixData[$row][$column] = $matrixCell; - ++$column; - } - if ($column > $maxColumn) { - $maxColumn = $column; - } - ++$row; - } - - $matrix = new Matrix($matrixData); - if (!$matrix->isSquare()) { - return Functions::VALUE(); - } - - try { - return $matrix->determinant(); - } catch (MatrixException $ex) { - return Functions::VALUE(); - } + return MathTrig\MatrixFunctions::funcMDeterm($matrixValues); } /** @@ -614,6 +485,8 @@ class MathTrig * * Returns the inverse matrix for the matrix stored in an array. * + * @Deprecated 2.0.0 Use the funcMInverse method in the MathTrig\MatrixFuncs class instead + * * Excel Function: * MINVERSE(array) * @@ -623,49 +496,14 @@ class MathTrig */ public static function MINVERSE($matrixValues) { - $matrixData = []; - if (!is_array($matrixValues)) { - $matrixValues = [[$matrixValues]]; - } - - $row = $maxColumn = 0; - foreach ($matrixValues as $matrixRow) { - if (!is_array($matrixRow)) { - $matrixRow = [$matrixRow]; - } - $column = 0; - foreach ($matrixRow as $matrixCell) { - if ((is_string($matrixCell)) || ($matrixCell === null)) { - return Functions::VALUE(); - } - $matrixData[$row][$column] = $matrixCell; - ++$column; - } - if ($column > $maxColumn) { - $maxColumn = $column; - } - ++$row; - } - - $matrix = new Matrix($matrixData); - if (!$matrix->isSquare()) { - return Functions::VALUE(); - } - - if ($matrix->determinant() == 0.0) { - return Functions::NAN(); - } - - try { - return $matrix->inverse()->toArray(); - } catch (MatrixException $ex) { - return Functions::VALUE(); - } + return MathTrig\MatrixFunctions::funcMInverse($matrixValues); } /** * MMULT. * + * @Deprecated 2.0.0 Use the funcMMult method in the MathTrig\MatrixFuncs class instead + * * @param array $matrixData1 A matrix of values * @param array $matrixData2 A matrix of values * @@ -673,56 +511,7 @@ class MathTrig */ public static function MMULT($matrixData1, $matrixData2) { - $matrixAData = $matrixBData = []; - if (!is_array($matrixData1)) { - $matrixData1 = [[$matrixData1]]; - } - if (!is_array($matrixData2)) { - $matrixData2 = [[$matrixData2]]; - } - - try { - $rowA = 0; - foreach ($matrixData1 as $matrixRow) { - if (!is_array($matrixRow)) { - $matrixRow = [$matrixRow]; - } - $columnA = 0; - foreach ($matrixRow as $matrixCell) { - if ((!is_numeric($matrixCell)) || ($matrixCell === null)) { - return Functions::VALUE(); - } - $matrixAData[$rowA][$columnA] = $matrixCell; - ++$columnA; - } - ++$rowA; - } - $matrixA = new Matrix($matrixAData); - $rowB = 0; - foreach ($matrixData2 as $matrixRow) { - if (!is_array($matrixRow)) { - $matrixRow = [$matrixRow]; - } - $columnB = 0; - foreach ($matrixRow as $matrixCell) { - if ((!is_numeric($matrixCell)) || ($matrixCell === null)) { - return Functions::VALUE(); - } - $matrixBData[$rowB][$columnB] = $matrixCell; - ++$columnB; - } - ++$rowB; - } - $matrixB = new Matrix($matrixBData); - - if ($columnA != $rowB) { - return Functions::VALUE(); - } - - return $matrixA->multiply($matrixB)->toArray(); - } catch (MatrixException $ex) { - return Functions::VALUE(); - } + return MathTrig\MatrixFunctions::funcMMult($matrixData1, $matrixData2); } /** @@ -779,30 +568,7 @@ class MathTrig */ public static function MULTINOMIAL(...$args) { - $summer = 0; - $divisor = 1; - // Loop through arguments - foreach (Functions::flattenArray($args) as $arg) { - // Is it a numeric value? - if (is_numeric($arg)) { - if ($arg < 1) { - return Functions::NAN(); - } - $summer += floor($arg); - $divisor *= self::FACT($arg); - } else { - return Functions::VALUE(); - } - } - - // Return - if ($summer > 0) { - $summer = self::FACT($summer); - - return $summer / $divisor; - } - - return 0; + return MathTrig\Multinomial::funcMultinomial(...$args); } /** @@ -854,36 +620,18 @@ class MathTrig * * PRODUCT returns the product of all the values and cells referenced in the argument list. * + * @Deprecated 2.0.0 Use the funcProduct method in the MathTrig\Product class instead + * * Excel Function: * PRODUCT(value1[,value2[, ...]]) * * @param mixed ...$args Data values * - * @return float + * @return float|string */ public static function PRODUCT(...$args) { - // Return value - $returnValue = null; - - // Loop through arguments - foreach (Functions::flattenArray($args) as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - if ($returnValue === null) { - $returnValue = $arg; - } else { - $returnValue *= $arg; - } - } - } - - // Return - if ($returnValue === null) { - return 0; - } - - return $returnValue; + return MathTrig\Product::funcProduct(...$args); } /** @@ -892,36 +640,19 @@ class MathTrig * QUOTIENT function returns the integer portion of a division. Numerator is the divided number * and denominator is the divisor. * + * @Deprecated 2.0.0 Use the funcQuotient method in the MathTrig\Quotient class instead + * * Excel Function: * QUOTIENT(value1[,value2[, ...]]) * - * @param mixed ...$args Data values + * @param mixed $numerator + * @param mixed $denominator * - * @return float + * @return int|string */ - public static function QUOTIENT(...$args) + public static function QUOTIENT($numerator, $denominator) { - // Return value - $returnValue = null; - - // Loop through arguments - foreach (Functions::flattenArray($args) as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - if ($returnValue === null) { - $returnValue = ($arg == 0) ? 0 : $arg; - } else { - if (($returnValue == 0) || ($arg == 0)) { - $returnValue = 0; - } else { - $returnValue /= $arg; - } - } - } - } - - // Return - return (int) $returnValue; + return MathTrig\Quotient::funcQuotient($numerator, $denominator); } /** @@ -1006,37 +737,18 @@ class MathTrig * * Returns the sum of a power series * - * @param mixed[] $args An array of mixed values for the Data Series + * @Deprecated 2.0.0 Use the funcSeriesSum method in the MathTrig\SeriesSum class instead + * + * @param mixed $x Input value + * @param mixed $n Initial power + * @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 */ - public static function SERIESSUM(...$args) + public static function SERIESSUM($x, $n, $m, ...$args) { - $returnValue = 0; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - - $x = array_shift($aArgs); - $n = array_shift($aArgs); - $m = array_shift($aArgs); - - if ((is_numeric($x)) && (is_numeric($n)) && (is_numeric($m))) { - // Calculate - $i = 0; - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $returnValue += $arg * $x ** ($n + ($m * $i++)); - } else { - return Functions::VALUE(); - } - } - - return $returnValue; - } - - return Functions::VALUE(); + return MathTrig\SeriesSum::funcSeriesSum($x, $n, $m, ...$args); } /** @@ -1090,45 +802,13 @@ class MathTrig return Functions::VALUE(); } - protected static function filterHiddenArgs($cellReference, $args) - { - return array_filter( - $args, - function ($index) use ($cellReference) { - [, $row, $column] = explode('.', $index); - - return $cellReference->getWorksheet()->getRowDimension($row)->getVisible() && - $cellReference->getWorksheet()->getColumnDimension($column)->getVisible(); - }, - ARRAY_FILTER_USE_KEY - ); - } - - protected static function filterFormulaArgs($cellReference, $args) - { - return array_filter( - $args, - function ($index) use ($cellReference) { - [, $row, $column] = explode('.', $index); - 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()); - - return !$isFormula || $cellFormula; - } - - return true; - }, - ARRAY_FILTER_USE_KEY - ); - } - /** * SUBTOTAL. * * Returns a subtotal in a list or database. * + * @Deprecated 2.0.0 Use the funcSubtotal method in the MathTrig\Subtotal class instead + * * @param int $functionType * A number 1 to 11 that specifies which function to * use in calculating subtotals within a range @@ -1142,45 +822,7 @@ class MathTrig */ public static function SUBTOTAL($functionType, ...$args) { - $cellReference = array_pop($args); - $aArgs = Functions::flattenArrayIndexed($args); - $subtotal = Functions::flattenSingleValue($functionType); - - // Calculate - if ((is_numeric($subtotal)) && (!is_string($subtotal))) { - if ($subtotal > 100) { - $aArgs = self::filterHiddenArgs($cellReference, $aArgs); - $subtotal -= 100; - } - - $aArgs = self::filterFormulaArgs($cellReference, $aArgs); - switch ($subtotal) { - case 1: - return Statistical\Averages::average($aArgs); - case 2: - return Statistical\Counts::COUNT($aArgs); - case 3: - return Statistical\Counts::COUNTA($aArgs); - case 4: - return Statistical\Maximum::MAX($aArgs); - case 5: - return Statistical\Minimum::MIN($aArgs); - case 6: - return self::PRODUCT($aArgs); - case 7: - return Statistical\StandardDeviations::STDEV($aArgs); - case 8: - return Statistical\StandardDeviations::STDEVP($aArgs); - case 9: - return self::SUM($aArgs); - case 10: - return Statistical\Variances::VAR($aArgs); - case 11: - return Statistical\Variances::VARP($aArgs); - } - } - - return Functions::VALUE(); + return MathTrig\Subtotal::funcSubtotal($functionType, ...$args); } /** @@ -1188,28 +830,18 @@ class MathTrig * * SUM computes the sum of all the values and cells referenced in the argument list. * + * @Deprecated 2.0.0 Use the funcSumNoStrings method in the MathTrig\Sum class instead + * * Excel Function: * SUM(value1[,value2[, ...]]) * * @param mixed ...$args Data values * - * @return float + * @return float|string */ public static function SUM(...$args) { - $returnValue = 0; - - // Loop through the arguments - foreach (Functions::flattenArray($args) as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $returnValue += $arg; - } elseif (Functions::isError($arg)) { - return $arg; - } - } - - return $returnValue; + return MathTrig\Sum::funcSum(...$args); } /** @@ -1229,7 +861,7 @@ class MathTrig * @param string $criteria the criteria that defines which cells will be summed * @param mixed $sumRange * - * @return float + * @return float|string */ public static function SUMIF($range, $criteria, $sumRange = []) { @@ -1251,7 +883,7 @@ class MathTrig * * @param mixed $args Data values * - * @return float + * @return float|string */ public static function SUMIFS(...$args) { @@ -1264,39 +896,15 @@ class MathTrig * Excel Function: * SUMPRODUCT(value1[,value2[, ...]]) * + * @Deprecated 2.0.0 Use the funcSumProduct method in the MathTrig\SumProduct class instead + * * @param mixed ...$args Data values * * @return float|string The result, or a string containing an error */ public static function SUMPRODUCT(...$args) { - $arrayList = $args; - - $wrkArray = Functions::flattenArray(array_shift($arrayList)); - $wrkCellCount = count($wrkArray); - - for ($i = 0; $i < $wrkCellCount; ++$i) { - if ((!is_numeric($wrkArray[$i])) || (is_string($wrkArray[$i]))) { - $wrkArray[$i] = 0; - } - } - - foreach ($arrayList as $matrixData) { - $array2 = Functions::flattenArray($matrixData); - $count = count($array2); - if ($wrkCellCount != $count) { - return Functions::VALUE(); - } - - foreach ($array2 as $i => $val) { - if ((!is_numeric($val)) || (is_string($val))) { - $val = 0; - } - $wrkArray[$i] *= $val; - } - } - - return array_sum($wrkArray); + return MathTrig\SumProduct::funcSumProduct(...$args); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Base.php b/src/PhpSpreadsheet/Calculation/MathTrig/Base.php new file mode 100644 index 00000000..35522334 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Base.php @@ -0,0 +1,49 @@ +getMessage(); + } + $minLength = Functions::flattenSingleValue($minLength); + + if ($minLength === null || is_numeric($minLength)) { + if ($number < 0 || $number >= 2 ** 53 || $radix < 2 || $radix > 36) { + return Functions::NAN(); // Numeric range constraints + } + + $outcome = strtoupper((string) base_convert($number, 10, $radix)); + if ($minLength !== null) { + $outcome = str_pad($outcome, (int) $minLength, '0', STR_PAD_LEFT); // String padding + } + + return $outcome; + } + + return Functions::VALUE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Fact.php b/src/PhpSpreadsheet/Calculation/MathTrig/Fact.php new file mode 100644 index 00000000..0d591b77 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Fact.php @@ -0,0 +1,47 @@ +getMessage(); + } + + $factLoop = floor($factVal); + if ($factVal > $factLoop) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + return Statistical::GAMMAFunction($factVal + 1); + } + } + + $factorial = 1; + while ($factLoop > 1) { + $factorial *= $factLoop--; + } + + return $factorial; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php index 63b5082c..7abcf050 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -61,6 +61,34 @@ class Helpers throw new Exception(Functions::VALUE()); } + /** + * Confirm number >= 0. + * + * @param float|int $number + */ + public static function validateNotNegative($number): void + { + if ($number >= 0) { + return; + } + + throw new Exception(Functions::NAN()); + } + + /** + * Confirm number != 0. + * + * @param float|int $number + */ + public static function validateNotZero($number): void + { + if ($number) { + return; + } + + throw new Exception(Functions::DIV0()); + } + public static function returnSign(float $number): int { return $number ? (($number > 0) ? 1 : -1) : 0; diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php b/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php new file mode 100644 index 00000000..3d3f0e46 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php @@ -0,0 +1,95 @@ + 1; --$i) { + if (($value % $i) == 0) { + $factorArray = array_merge($factorArray, self::factors($value / $i)); + $factorArray = array_merge($factorArray, self::factors($i)); + if ($i <= sqrt($value)) { + break; + } + } + } + if (!empty($factorArray)) { + rsort($factorArray); + + return $factorArray; + } + + return [(int) $value]; + } + + /** + * LCM. + * + * Returns the lowest common multiplier of a series of numbers + * The least common multiple is the smallest positive integer that is a multiple + * of all integer arguments number1, number2, and so on. Use LCM to add fractions + * with different denominators. + * + * Excel Function: + * LCM(number1[,number2[, ...]]) + * + * @param mixed ...$args Data values + * + * @return int|string Lowest Common Multiplier, or a string containing an error + */ + public static function funcLcm(...$args) + { + try { + $arrayArgs = []; + $anyZeros = 0; + foreach (Functions::flattenArray($args) as $value1) { + $value = Helpers::validateNumericNullSubstitution($value1, 1); + Helpers::validateNotNegative($value); + $arrayArgs[] = (int) $value; + $anyZeros += (int) !((bool) $value); + } + if ($anyZeros) { + return 0; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + $returnValue = 1; + $allPoweredFactors = []; + // Loop through arguments + foreach ($arrayArgs as $value) { + $myFactors = self::factors(floor($value)); + $myCountedFactors = array_count_values($myFactors); + $myPoweredFactors = []; + foreach ($myCountedFactors as $myCountedFactor => $myCountedPower) { + $myPoweredFactors[$myCountedFactor] = $myCountedFactor ** $myCountedPower; + } + foreach ($myPoweredFactors as $myPoweredValue => $myPoweredFactor) { + if (isset($allPoweredFactors[$myPoweredValue])) { + if ($allPoweredFactors[$myPoweredValue] < $myPoweredFactor) { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } else { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } + } + foreach ($allPoweredFactors as $allPoweredFactor) { + $returnValue *= (int) $allPoweredFactor; + } + + return $returnValue; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php b/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php new file mode 100644 index 00000000..77c8b1e1 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php @@ -0,0 +1,114 @@ +determinant(); + } catch (MatrixException $ex) { + return Functions::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MINVERSE. + * + * Returns the inverse matrix for the matrix stored in an array. + * + * Excel Function: + * MINVERSE(array) + * + * @param mixed $matrixValues A matrix of values + * + * @return array|string The result, or a string containing an error + */ + public static function funcMInverse($matrixValues) + { + try { + $matrix = self::getMatrix($matrixValues); + + return $matrix->inverse()->toArray(); + } catch (MatrixException $e) { + return (strpos($e->getMessage(), 'determinant') === false) ? Functions::VALUE() : Functions::NAN(); + } catch (Exception $e) { + return $e->getMessage(); + } + } + + /** + * MMULT. + * + * @param mixed $matrixData1 A matrix of values + * @param mixed $matrixData2 A matrix of values + * + * @return array|string The result, or a string containing an error + */ + public static function funcMMult($matrixData1, $matrixData2) + { + try { + $matrixA = self::getMatrix($matrixData1); + $matrixB = self::getMatrix($matrixData2); + + return $matrixA->multiply($matrixB)->toArray(); + } catch (MatrixException $ex) { + return Functions::VALUE(); + } catch (Exception $e) { + return $e->getMessage(); + } + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Multinomial.php b/src/PhpSpreadsheet/Calculation/MathTrig/Multinomial.php new file mode 100644 index 00000000..5ebecb97 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Multinomial.php @@ -0,0 +1,41 @@ +getMessage(); + } + + $summer = Fact::funcFact($summer); + + return $summer / $divisor; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Product.php b/src/PhpSpreadsheet/Calculation/MathTrig/Product.php new file mode 100644 index 00000000..254b7b79 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Product.php @@ -0,0 +1,47 @@ +getMessage(); + } + + return (int) ($numerator / $denominator); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php b/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php new file mode 100644 index 00000000..063593b6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/SeriesSum.php @@ -0,0 +1,46 @@ +getMessage(); + } + + return $returnValue; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php b/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php new file mode 100644 index 00000000..3d441fa2 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Subtotal.php @@ -0,0 +1,99 @@ +getWorksheet()->getRowDimension($row)->getVisible(); + }, + ARRAY_FILTER_USE_KEY + ); + } + + protected static function filterFormulaArgs($cellReference, $args) + { + return array_filter( + $args, + function ($index) use ($cellReference) { + [, $row, $column] = explode('.', $index); + $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()); + + $retVal = !$isFormula || $cellFormula; + } + + return $retVal; + }, + ARRAY_FILTER_USE_KEY + ); + } + + 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 + [Product::class, 'funcProduct'], // 6 + [Statistical\StandardDeviations::class, 'STDEV'], // 7 + [Statistical\StandardDeviations::class, 'STDEVP'], // 8 + [Sum::class, 'funcSum'], // 9 + [Statistical\Variances::class, 'VAR'], // 10 + [Statistical\Variances::class, 'VARP'], // 11 + ]; + + /** + * SUBTOTAL. + * + * Returns a subtotal in a list or database. + * + * @param mixed $functionType + * A number 1 to 11 that specifies which function to + * use in calculating subtotals within a range + * list + * Numbers 101 to 111 shadow the functions of 1 to 11 + * but ignore any values in the range that are + * in hidden rows or columns + * @param mixed[] $args A mixed data series of values + * + * @return float|string + */ + public static function funcSubtotal($functionType, ...$args) + { + $cellReference = array_pop($args); + $aArgs = Functions::flattenArrayIndexed($args); + + try { + $subtotal = (int) Helpers::validateNumericNullBool($functionType); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Calculate + if ($subtotal > 100) { + $aArgs = self::filterHiddenArgs($cellReference, $aArgs); + $subtotal -= 100; + } + + $aArgs = self::filterFormulaArgs($cellReference, $aArgs); + if (array_key_exists($subtotal, self::CALL_FUNCTIONS)) { + return call_user_func_array(self::CALL_FUNCTIONS[$subtotal], $aArgs); + } + + return Functions::VALUE(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php new file mode 100644 index 00000000..cd29248b --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sum.php @@ -0,0 +1,68 @@ + $val) { + if ((!is_numeric($val)) || (is_string($val))) { + $val = 0; + } + $wrkArray[$i] *= $val; + } + } + + return array_sum($wrkArray); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php index 7ed1e714..51e6b004 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Conditional.php @@ -178,7 +178,7 @@ class Conditional * @param mixed $sumRange * @param mixed $condition * - * @return float + * @return float|string */ public static function SUMIF($range, $condition, $sumRange = []) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php index 9dd6a49d..f9bdac46 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcosTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AcosTest extends TestCase +class AcosTest extends AllSetupTeardown { /** * @dataProvider providerAcos @@ -15,11 +11,8 @@ class AcosTest extends TestCase */ public function testAcos($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A2')->setValue(0.5); $sheet->getCell('A1')->setValue("=ACOS($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php index d596cc9e..40930582 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcoshTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AcoshTest extends TestCase +class AcoshTest extends AllSetupTeardown { /** * @dataProvider providerAcosh @@ -15,11 +11,8 @@ class AcoshTest extends TestCase */ public function testAcosh($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A2')->setValue('1.5'); $sheet->getCell('A1')->setValue("=ACOSH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php index 99694215..de9e2196 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcotTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AcotTest extends TestCase +class AcotTest extends AllSetupTeardown { /** * @dataProvider providerACOT @@ -16,11 +12,8 @@ class AcotTest extends TestCase */ public function testACOT($expectedResult, $number): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php index 1d565e73..f28064f8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AcothTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AcothTest extends TestCase +class AcothTest extends AllSetupTeardown { /** * @dataProvider providerACOTH @@ -16,11 +12,8 @@ class AcothTest extends TestCase */ public function testACOTH($expectedResult, $number): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php new file mode 100644 index 00000000..86c30c22 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php @@ -0,0 +1,52 @@ +compatibilityMode = Functions::getCompatibilityMode(); + $this->spreadsheet = new Spreadsheet(); + $this->sheet = $this->spreadsheet->getActiveSheet(); + } + + protected function tearDown(): void + { + Functions::setCompatibilityMode($this->compatibilityMode); + $this->spreadsheet->disconnectWorksheets(); + $this->spreadsheet = null; + $this->sheet = null; + } + + protected static function setOpenOffice(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + } + + protected static function setGnumeric(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); + } + + /** + * @param mixed $expectedResult + */ + protected function mightHaveException($expectedResult): void + { + if ($expectedResult === 'exception') { + $this->expectException(CalcException::class); + } + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php index c1c836f3..4797a93f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AsinTest extends TestCase +class AsinTest extends AllSetupTeardown { /** * @dataProvider providerAsin @@ -15,11 +11,8 @@ class AsinTest extends TestCase */ public function testAsin($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A2')->setValue(0.5); $sheet->getCell('A1')->setValue("=ASIN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php index ebbb74f1..3f63a01c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AsinhTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AsinhTest extends TestCase +class AsinhTest extends AllSetupTeardown { /** * @dataProvider providerAsinh @@ -15,11 +11,8 @@ class AsinhTest extends TestCase */ public function testAsinh($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A2')->setValue(0.5); $sheet->getCell('A1')->setValue("=ASINH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php index 35a96aea..e1d435de 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Atan2Test.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class Atan2Test extends TestCase +class Atan2Test extends AllSetupTeardown { /** * @dataProvider providerATAN2 @@ -15,11 +11,8 @@ class Atan2Test extends TestCase */ public function testATAN2($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A2')->setValue(5); $sheet->getCell('A3')->setValue(6); $sheet->getCell('A1')->setValue("=ATAN2($formula)"); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php index 4dec2dca..c92f834a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AtanTest extends TestCase +class AtanTest extends AllSetupTeardown { /** * @dataProvider providerAtan @@ -15,11 +11,8 @@ class AtanTest extends TestCase */ public function testAtan($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A2')->setValue(5); $sheet->getCell('A1')->setValue("=ATAN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php index cc8a243f..abefd334 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AtanhTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AtanhTest extends TestCase +class AtanhTest extends AllSetupTeardown { /** * @dataProvider providerAtanh @@ -15,11 +11,8 @@ class AtanhTest extends TestCase */ public function testAtanh($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A2')->setValue(0.8); $sheet->getCell('A1')->setValue("=ATANH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/BaseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/BaseTest.php index 72b52559..94176c77 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/BaseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/BaseTest.php @@ -2,25 +2,39 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class BaseTest extends TestCase +class BaseTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerBASE * * @param mixed $expectedResult + * @param mixed $arg1 + * @param mixed $arg2 + * @param mixed $arg3 */ - public function testBASE($expectedResult, ...$args): void + public function testBASE($expectedResult, $arg1 = 'omitted', $arg2 = 'omitted', $arg3 = 'omitted'): void { - $result = MathTrig::BASE(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('A1')->setValue($arg1); + } + if ($arg2 !== null) { + $sheet->getCell('A2')->setValue($arg2); + } + if ($arg3 !== null) { + $sheet->getCell('A3')->setValue($arg3); + } + if ($arg1 === 'omitted') { + $sheet->getCell('B1')->setValue('=BASE()'); + } elseif ($arg2 === 'omitted') { + $sheet->getCell('B1')->setValue('=BASE(A1)'); + } elseif ($arg3 === 'omitted') { + $sheet->getCell('B1')->setValue('=BASE(A1, A2)'); + } else { + $sheet->getCell('B1')->setValue('=BASE(A1, A2, A3)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEquals($expectedResult, $result); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingMathTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingMathTest.php index 1d62b8c3..cd1dcb33 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingMathTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingMathTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CeilingMathTest extends TestCase +class CeilingMathTest extends AllSetupTeardown { /** * @dataProvider providerCEILINGMATH @@ -16,11 +12,8 @@ class CeilingMathTest extends TestCase */ public function testCEILINGMATH($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingPreciseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingPreciseTest.php index d47670b7..7c9e289e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingPreciseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingPreciseTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CeilingPreciseTest extends TestCase +class CeilingPreciseTest extends AllSetupTeardown { /** * @dataProvider providerFLOORPRECISE @@ -16,11 +12,8 @@ class CeilingPreciseTest extends TestCase */ public function testCEILINGPRECISE($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingTest.php index ad99b5f0..bbbc10ea 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CeilingTest.php @@ -2,26 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CeilingTest extends TestCase +class CeilingTest extends AllSetupTeardown { - private $compatibilityMode; - - protected function setUp(): void - { - $this->compatibilityMode = Functions::getCompatibilityMode(); - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - - protected function tearDown(): void - { - Functions::setCompatibilityMode($this->compatibilityMode); - } - /** * @dataProvider providerCEILING * @@ -30,11 +12,8 @@ class CeilingTest extends TestCase */ public function testCEILING($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); @@ -51,9 +30,8 @@ class CeilingTest extends TestCase public function testCEILINGGnumeric1Arg(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + self::setGnumeric(); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue('=CEILING(5.1)'); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta(6, $result, 1E-12); @@ -61,20 +39,17 @@ class CeilingTest extends TestCase public function testCELINGOpenOffice1Arg(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + self::setOpenOffice(); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue('=CEILING(5.1)'); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta(6, $result, 1E-12); } - public function testFLOORExcel1Arg(): void + public function testCEILINGExcel1Arg(): void { - $this->expectException(CalcExp::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->mightHaveException('exception'); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue('=CEILING(5.1)'); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta(6, $result, 1E-12); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php index d5ada718..3a31b454 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CosTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CosTest extends TestCase +class CosTest extends AllSetupTeardown { /** * @dataProvider providerCos @@ -15,11 +11,8 @@ class CosTest extends TestCase */ public function testCos($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 2); $sheet->getCell('A1')->setValue("=COS($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php index 81dc9c75..83c9315c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CoshTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CoshTest extends TestCase +class CoshTest extends AllSetupTeardown { /** * @dataProvider providerCosh @@ -15,11 +11,8 @@ class CoshTest extends TestCase */ public function testCosh($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 2); $sheet->getCell('A1')->setValue("=COSH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php index cb009a89..5ed9ac78 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CotTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CotTest extends TestCase +class CotTest extends AllSetupTeardown { /** * @dataProvider providerCOT @@ -16,11 +12,8 @@ class CotTest extends TestCase */ public function testCOT($expectedResult, $angle): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php index e4b42a4d..515ad0a8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CothTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CothTest extends TestCase +class CothTest extends AllSetupTeardown { /** * @dataProvider providerCOTH @@ -16,11 +12,8 @@ class CothTest extends TestCase */ public function testCOTH($expectedResult, $angle): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php index 8ae48cde..3b401ef2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CscTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CscTest extends TestCase +class CscTest extends AllSetupTeardown { /** * @dataProvider providerCSC @@ -16,11 +12,8 @@ class CscTest extends TestCase */ public function testCSC($expectedResult, $angle): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php index 4a7dbc05..7cf33099 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CschTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class CschTest extends TestCase +class CschTest extends AllSetupTeardown { /** * @dataProvider providerCSCH @@ -16,11 +12,8 @@ class CschTest extends TestCase */ public function testCSCH($expectedResult, $angle): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php index 080925b1..c8cc8645 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class EvenTest extends TestCase +class EvenTest extends AllSetupTeardown { /** * @dataProvider providerEVEN @@ -16,11 +12,8 @@ class EvenTest extends TestCase */ public function testEVEN($expectedResult, $value): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue("=EVEN($value)"); $sheet->getCell('A2')->setValue(3.7); self::assertEquals($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php index f6092896..855e7605 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactTest.php @@ -2,31 +2,60 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class FactTest extends TestCase +class FactTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerFACT * * @param mixed $expectedResult - * @param $value + * @param mixed $arg1 */ - public function testFACT($expectedResult, $value): void + public function testFACT($expectedResult, $arg1): void { - $result = MathTrig::FACT($value); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('A1')->setValue($arg1); + } + if ($arg1 === 'omitted') { + $sheet->getCell('B1')->setValue('=FACT()'); + } else { + $sheet->getCell('B1')->setValue('=FACT(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); } public function providerFACT() { return require 'tests/data/Calculation/MathTrig/FACT.php'; } + + /** + * @dataProvider providerFACTGnumeric + * + * @param mixed $expectedResult + * @param mixed $arg1 + */ + public function testFACTGnumeric($expectedResult, $arg1): void + { + $this->mightHaveException($expectedResult); + self::setGnumeric(); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('A1')->setValue($arg1); + } + if ($arg1 === 'omitted') { + $sheet->getCell('B1')->setValue('=FACT()'); + } else { + $sheet->getCell('B1')->setValue('=FACT(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerFACTGnumeric() + { + return require 'tests/data/Calculation/MathTrig/FACTGNUMERIC.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorMathTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorMathTest.php index ce546159..35ddd892 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorMathTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorMathTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class FloorMathTest extends TestCase +class FloorMathTest extends AllSetupTeardown { /** * @dataProvider providerFLOORMATH @@ -16,11 +12,8 @@ class FloorMathTest extends TestCase */ public function testFLOORMATH($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorPreciseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorPreciseTest.php index 961ca8ae..bf580134 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorPreciseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorPreciseTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class FloorPreciseTest extends TestCase +class FloorPreciseTest extends AllSetupTeardown { /** * @dataProvider providerFLOORPRECISE @@ -16,11 +12,8 @@ class FloorPreciseTest extends TestCase */ public function testFLOORPRECISE($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorTest.php index 82a142c2..d684e84f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FloorTest.php @@ -2,26 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class FloorTest extends TestCase +class FloorTest extends AllSetupTeardown { - private $compatibilityMode; - - protected function setUp(): void - { - $this->compatibilityMode = Functions::getCompatibilityMode(); - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - - protected function tearDown(): void - { - Functions::setCompatibilityMode($this->compatibilityMode); - } - /** * @dataProvider providerFLOOR * @@ -30,11 +12,8 @@ class FloorTest extends TestCase */ public function testFLOOR($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); @@ -51,9 +30,8 @@ class FloorTest extends TestCase public function testFLOORGnumeric1Arg(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_GNUMERIC); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + self::setGnumeric(); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue('=FLOOR(5.1)'); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta(5, $result, 1E-12); @@ -61,9 +39,8 @@ class FloorTest extends TestCase public function testFLOOROpenOffice1Arg(): void { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + self::setOpenOffice(); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue('=FLOOR(5.1)'); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta(5, $result, 1E-12); @@ -71,10 +48,8 @@ class FloorTest extends TestCase public function testFLOORExcel1Arg(): void { - $this->expectException(CalcExp::class); - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + $this->mightHaveException('exception'); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue('=FLOOR(5.1)'); $result = $sheet->getCell('A1')->getCalculatedValue(); self::assertEqualsWithDelta(5, $result, 1E-12); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/IntTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/IntTest.php index 5c0b12c8..989b5bd1 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/IntTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/IntTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class IntTest extends TestCase +class IntTest extends AllSetupTeardown { /** * @dataProvider providerINT @@ -16,11 +12,8 @@ class IntTest extends TestCase */ public function testINT($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LcmTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LcmTest.php index 57b4a67f..55655d83 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LcmTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LcmTest.php @@ -2,17 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class LcmTest extends TestCase +class LcmTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerLCM * @@ -20,7 +11,14 @@ class LcmTest extends TestCase */ public function testLCM($expectedResult, ...$args): void { - $result = MathTrig::LCM(...$args); + $sheet = $this->sheet; + $row = 0; + foreach ($args as $arg) { + ++$row; + $sheet->getCell("A$row")->setValue($arg); + } + $sheet->getCell('B1')->setValue("=LCM(A1:A$row)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php index a500c3f6..8831fe83 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php @@ -2,17 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; -class MInverseTest extends TestCase +class MInverseTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerMINVERSE * @@ -28,4 +21,12 @@ class MInverseTest extends TestCase { return require 'tests/data/Calculation/MathTrig/MINVERSE.php'; } + + public function testOnSpreadsheet(): void + { + // very limited ability to test this in the absence of dynamic arrays + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue('=MINVERSE({1,2,3})'); // not square + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php index 66fa80db..ca8ee5d7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php @@ -2,17 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; -class MMultTest extends TestCase +class MMultTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerMMULT * @@ -28,4 +21,12 @@ class MMultTest extends TestCase { return require 'tests/data/Calculation/MathTrig/MMULT.php'; } + + public function testOnSpreadsheet(): void + { + // very limited ability to test this in the absence of dynamic arrays + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue('=MMULT({1,2,3}, {1,2,3})'); // incompatible dimensions + self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MRoundTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MRoundTest.php index 87554d06..404193e9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MRoundTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MRoundTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class MRoundTest extends TestCase +class MRoundTest extends AllSetupTeardown { /** * @dataProvider providerMROUND @@ -16,11 +12,8 @@ class MRoundTest extends TestCase */ public function testMROUND($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MdeTermTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MdeTermTest.php index 995ea2f3..b86a90c0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MdeTermTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MdeTermTest.php @@ -2,25 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class MdeTermTest extends TestCase +class MdeTermTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerMDETERM * * @param mixed $expectedResult + * @param mixed $matrix expect a matrix */ - public function testMDETERM($expectedResult, ...$args): void + public function testMDETERM2($expectedResult, $matrix): void { - $result = MathTrig::MDETERM(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if (is_array($matrix)) { + $sheet->fromArray($matrix, null, 'A1', true); + $maxCol = $sheet->getHighestColumn(); + $maxRow = $sheet->getHighestRow(); + $sheet->getCell('Z1')->setValue("=MDETERM(A1:$maxCol$maxRow)"); + } else { + $sheet->getCell('Z1')->setValue("=MDETERM($matrix)"); + } + $result = $sheet->getCell('Z1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php index 45c558cd..5b3642ff 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php @@ -25,6 +25,7 @@ class MovedFunctionsTest extends TestCase self::assertEqualsWithDelta(0, MathTrig::builtinATAN(0), 1E-9); self::assertEqualsWithDelta(0, MathTrig::builtinATANH(0), 1E-9); self::assertEqualsWithDelta('#DIV/0!', MathTrig::ATAN2(0, 0), 1E-9); + self::assertEquals('12', MathTrig::BASE(10, 8)); self::assertEquals(-6, MathTrig::CEILING(-4.5, -2)); self::assertEquals(1, MathTrig::builtinCOS(0)); self::assertEquals(1, MathTrig::builtinCOSH(0)); @@ -33,20 +34,40 @@ class MovedFunctionsTest extends TestCase self::assertEquals('#DIV/0!', MathTrig::CSC(0)); self::assertEquals('#DIV/0!', MathTrig::CSCH(0)); self::assertEquals(6, MathTrig::EVEN(4.5)); + self::assertEquals(6, MathTrig::FACT(3)); self::assertEquals(-6, MathTrig::FLOOR(-4.5, 2)); self::assertEquals(0.23, MathTrig::FLOORMATH(0.234, 0.01)); self::assertEquals(-4, MathTrig::FLOORPRECISE(-2.5, 2)); self::assertEquals(-9, MathTrig::INT(-8.3)); + self::assertEquals(12, MathTrig::LCM(4, 6)); + self::assertEquals(1, MathTrig::MDETERM([1])); + self::assertEquals( + [[2, 2], [2, 1]], + MathTrig::MINVERSE([[-0.5, 1.0], [1.0, -1.0]]) + ); + self::assertEquals( + [[23], [53]], + MathTrig::MMULT([[1, 2], [3, 4]], [[7], [8]]) + ); self::assertEquals(6, MathTrig::MROUND(7.3, 3)); + self::assertEquals(1, MathTrig::MULTINOMIAL(1)); self::assertEquals(5, MathTrig::ODD(4.5)); + self::assertEquals(8, MathTrig::PRODUCT(1, 2, 4)); + self::assertEquals(8, MathTrig::QUOTIENT(17, 2)); + self::assertEquals('I', MathTrig::ROMAN(1)); self::assertEquals(3.3, MathTrig::builtinROUND(3.27, 1)); self::assertEquals(662, MathTrig::ROUNDDOWN(662.79, 0)); self::assertEquals(663, MathTrig::ROUNDUP(662.79, 0)); self::assertEquals(1, MathTrig::SEC(0)); self::assertEquals(1, MathTrig::SECH(0)); + self::assertEquals(3780, MathTrig::SERIESSUM(5, 1, 1, [1, 1, 0, 1, 1])); self::assertEquals(1, MathTrig::SIGN(79.2)); self::assertEquals(0, MathTrig::builtinSIN(0)); self::assertEquals(0, MathTrig::builtinSINH(0)); + self::assertEquals(0, MathTrig::SUBTOTAL(2, [0, 0])); + self::assertEquals(7, MathTrig::SUM(1, 2, 4)); + self::assertEquals(4, MathTrig::SUMIF([[2], [4]], '>2')); + self::assertEquals(17, MathTrig::SUMPRODUCT([1, 2, 3], [5, 0, 4])); self::assertEquals(0, MathTrig::builtinTAN(0)); self::assertEquals(0, MathTrig::builtinTANH(0)); self::assertEquals(70, MathTrig::TRUNC(79.2, -1)); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MultinomialTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MultinomialTest.php index 93735ba9..1c22cc40 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MultinomialTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MultinomialTest.php @@ -2,17 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class MultinomialTest extends TestCase +class MultinomialTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerMULTINOMIAL * @@ -20,7 +11,19 @@ class MultinomialTest extends TestCase */ public function testMULTINOMIAL($expectedResult, ...$args): void { - $result = MathTrig::MULTINOMIAL(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $row = 0; + $excelArg = ''; + foreach ($args as $arg) { + ++$row; + $excelArg = "A1:A$row"; + if ($arg !== null) { + $sheet->getCell("A$row")->setValue($arg); + } + } + $sheet->getCell('B1')->setValue("=MULTINOMIAL($excelArg)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php index ed262d9c..c599a30e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class OddTest extends TestCase +class OddTest extends AllSetupTeardown { /** * @dataProvider providerODD @@ -16,11 +12,8 @@ class OddTest extends TestCase */ public function testODD($expectedResult, $value): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->getCell('A1')->setValue("=ODD($value)"); $sheet->getCell('A2')->setValue(3.7); self::assertEquals($expectedResult, $sheet->getCell('A1')->getCalculatedValue()); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ProductTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ProductTest.php index 251b783b..c38eb130 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ProductTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ProductTest.php @@ -2,17 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class ProductTest extends TestCase +class ProductTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPRODUCT * @@ -20,7 +11,14 @@ class ProductTest extends TestCase */ public function testPRODUCT($expectedResult, ...$args): void { - $result = MathTrig::PRODUCT(...$args); + $sheet = $this->sheet; + $row = 0; + foreach ($args as $arg) { + ++$row; + $sheet->getCell("A$row")->setValue($arg); + } + $sheet->getCell('B1')->setValue("=PRODUCT(A1:A$row)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/QuotientTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/QuotientTest.php index 4232729a..3df2ed99 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/QuotientTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/QuotientTest.php @@ -2,26 +2,34 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class QuotientTest extends TestCase +class QuotientTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerQUOTIENT * * @param mixed $expectedResult + * @param mixed $arg1 + * @param mixed $arg2 */ - public function testQUOTIENT($expectedResult, ...$args): void + public function testQUOTIENT($expectedResult, $arg1 = 'omitted', $arg2 = 'omitted'): void { - $result = MathTrig::QUOTIENT(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('A1')->setValue($arg1); + } + if ($arg2 !== null) { + $sheet->getCell('A2')->setValue($arg2); + } + if ($arg1 === 'omitted') { + $sheet->getCell('B1')->setValue('=QUOTIENT()'); + } elseif ($arg2 === 'omitted') { + $sheet->getCell('B1')->setValue('=QUOTIENT(A1)'); + } else { + $sheet->getCell('B1')->setValue('=QUOTIENT(A1, A2)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); } public function providerQUOTIENT() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RomanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RomanTest.php index e913e5d7..0d71ece0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RomanTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RomanTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class RomanTest extends TestCase +class RomanTest extends AllSetupTeardown { /** * @dataProvider providerROMAN @@ -16,11 +12,8 @@ class RomanTest extends TestCase */ public function testROMAN($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A3', 49); $sheet->getCell('A1')->setValue("=ROMAN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); @@ -31,11 +24,4 @@ class RomanTest extends TestCase { return require 'tests/data/Calculation/MathTrig/ROMAN.php'; } - - // Confirm that deprecated stub left in MathTrig works. - // Delete this test when stub is finally deleted. - public function testDeprecated(): void - { - self::assertEquals('I', \PhpOffice\PhpSpreadsheet\Calculation\MathTrig::ROMAN(1)); - } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundDownTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundDownTest.php index 1ea1f7cb..e450c29e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundDownTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundDownTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class RoundDownTest extends TestCase +class RoundDownTest extends AllSetupTeardown { /** * @dataProvider providerRoundDown @@ -16,11 +12,8 @@ class RoundDownTest extends TestCase */ public function testRoundDown($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php index dd09bbaa..ee52b93d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class RoundTest extends TestCase +class RoundTest extends AllSetupTeardown { /** * @dataProvider providerRound @@ -16,11 +12,8 @@ class RoundTest extends TestCase */ public function testRound($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundUpTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundUpTest.php index 7907be42..6aa6c796 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundUpTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RoundUpTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class RoundUpTest extends TestCase +class RoundUpTest extends AllSetupTeardown { /** * @dataProvider providerRoundUp @@ -16,11 +12,8 @@ class RoundUpTest extends TestCase */ public function testRoundUp($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php index a47ae7b5..6a008102 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SecTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class SecTest extends TestCase +class SecTest extends AllSetupTeardown { /** * @dataProvider providerSEC @@ -16,11 +12,8 @@ class SecTest extends TestCase */ public function testSEC($expectedResult, $angle): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php index 65ed7b73..a93f37c5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SechTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class SechTest extends TestCase +class SechTest extends AllSetupTeardown { /** * @dataProvider providerSECH @@ -16,11 +12,8 @@ class SechTest extends TestCase */ public function testSECH($expectedResult, $angle): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SeriesSumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SeriesSumTest.php index 689336a3..86a40d07 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SeriesSumTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SeriesSumTest.php @@ -3,24 +3,39 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; -class SeriesSumTest extends TestCase +class SeriesSumTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSERIESSUM * * @param mixed $expectedResult + * @param mixed $arg1 + * @param mixed $arg2 + * @param mixed $arg3 */ - public function testSERIESSUM($expectedResult, ...$args): void + public function testSERIESSUM($expectedResult, $arg1, $arg2, $arg3, ...$args): void { - $result = MathTrig::SERIESSUM(...$args); + $sheet = $this->sheet; + if ($arg1 !== null) { + $sheet->getCell('C1')->setValue($arg1); + } + if ($arg2 !== null) { + $sheet->getCell('C2')->setValue($arg2); + } + if ($arg3 !== null) { + $sheet->getCell('C3')->setValue($arg3); + } + $row = 0; + $aArgs = Functions::flattenArray($args); + foreach ($aArgs as $arg) { + ++$row; + if ($arg !== null) { + $sheet->getCell("A$row")->setValue($arg); + } + } + $sheet->getCell('B1')->setValue("=SERIESSUM(C1, C2, C3, A1:A$row)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php index a4311219..dff5370d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class SignTest extends TestCase +class SignTest extends AllSetupTeardown { /** * @dataProvider providerSIGN @@ -16,11 +12,8 @@ class SignTest extends TestCase */ public function testSIGN($expectedResult, $value): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 0); $sheet->setCellValue('A4', -3.8); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php index e9ad6329..c460605f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class SinTest extends TestCase +class SinTest extends AllSetupTeardown { /** * @dataProvider providerSin @@ -15,11 +11,8 @@ class SinTest extends TestCase */ public function testSin($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 2); $sheet->getCell('A1')->setValue("=SIN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php index 38bfc7ef..30c40615 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SinhTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class SinhTest extends TestCase +class SinhTest extends AllSetupTeardown { /** * @dataProvider providerCosh @@ -15,11 +11,8 @@ class SinhTest extends TestCase */ public function testSinh($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 2); $sheet->getCell('A1')->setValue("=SINH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php index a629a7f4..7d44c551 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SubTotalTest.php @@ -2,53 +2,23 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PhpOffice\PhpSpreadsheet\Cell\Cell; -use PhpOffice\PhpSpreadsheet\Worksheet\ColumnDimension; -use PhpOffice\PhpSpreadsheet\Worksheet\RowDimension; -use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet; -use PHPUnit\Framework\TestCase; - -class SubTotalTest extends TestCase +class SubTotalTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUBTOTAL * * @param mixed $expectedResult + * @param mixed $type expect an integer */ - public function testSUBTOTAL($expectedResult, ...$args): void + public function testSubtotal($expectedResult, $type): void { - $cell = $this->getMockBuilder(Cell::class) - ->onlyMethods(['getValue', 'isFormula']) - ->disableOriginalConstructor() - ->getMock(); - $cell->method('getValue') - ->willReturn(null); - $cell->method('getValue') - ->willReturn(false); - $worksheet = $this->getMockBuilder(Worksheet::class) - ->onlyMethods(['cellExists', 'getCell']) - ->disableOriginalConstructor() - ->getMock(); - $worksheet->method('cellExists') - ->willReturn(true); - $worksheet->method('getCell') - ->willReturn($cell); - $cellReference = $this->getMockBuilder(Cell::class) - ->onlyMethods(['getWorksheet']) - ->disableOriginalConstructor() - ->getMock(); - $cellReference->method('getWorksheet') - ->willReturn($worksheet); - - array_push($args, $cellReference); - $result = MathTrig::SUBTOTAL(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->fromArray([[0], [1], [1], [2], [3], [5], [8], [13], [21], [34], [55], [89]], null, 'A1', true); + $maxCol = $sheet->getHighestColumn(); + $maxRow = $sheet->getHighestRow(); + $sheet->getCell('D2')->setValue("=SUBTOTAL($type, A1:$maxCol$maxRow)"); + $result = $sheet->getCell('D2')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } @@ -57,142 +27,102 @@ class SubTotalTest extends TestCase return require 'tests/data/Calculation/MathTrig/SUBTOTAL.php'; } - protected function rowVisibility($data) - { - foreach ($data as $row => $visibility) { - yield $row => $visibility; - } - } - /** - * @dataProvider providerHiddenSUBTOTAL + * @dataProvider providerSUBTOTAL * * @param mixed $expectedResult - * @param mixed $hiddenRows + * @param mixed $type expect an integer */ - public function testHiddenSUBTOTAL($expectedResult, $hiddenRows, ...$args): void + public function testSubtotalColumnHidden($expectedResult, $type): void { - $visibilityGenerator = $this->rowVisibility($hiddenRows); - - $rowDimension = $this->getMockBuilder(RowDimension::class) - ->onlyMethods(['getVisible']) - ->disableOriginalConstructor() - ->getMock(); - $rowDimension->method('getVisible') - ->willReturnCallback(function () use ($visibilityGenerator) { - $result = $visibilityGenerator->current(); - $visibilityGenerator->next(); - - return $result; - }); - $columnDimension = $this->getMockBuilder(ColumnDimension::class) - ->onlyMethods(['getVisible']) - ->disableOriginalConstructor() - ->getMock(); - $columnDimension->method('getVisible') - ->willReturn(true); - $cell = $this->getMockBuilder(Cell::class) - ->onlyMethods(['getValue', 'isFormula']) - ->disableOriginalConstructor() - ->getMock(); - $cell->method('getValue') - ->willReturn(''); - $cell->method('getValue') - ->willReturn(false); - $worksheet = $this->getMockBuilder(Worksheet::class) - ->onlyMethods(['cellExists', 'getCell', 'getRowDimension', 'getColumnDimension']) - ->disableOriginalConstructor() - ->getMock(); - $worksheet->method('cellExists') - ->willReturn(true); - $worksheet->method('getCell') - ->willReturn($cell); - $worksheet->method('getRowDimension') - ->willReturn($rowDimension); - $worksheet->method('getColumnDimension') - ->willReturn($columnDimension); - $cellReference = $this->getMockBuilder(Cell::class) - ->onlyMethods(['getWorksheet']) - ->disableOriginalConstructor() - ->getMock(); - $cellReference->method('getWorksheet') - ->willReturn($worksheet); - - array_push($args, $cellReference); - $result = MathTrig::SUBTOTAL(...$args); + // Hidden columns don't affect calculation, only hidden rows + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->fromArray([0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89], null, 'A1', true); + $maxCol = $sheet->getHighestColumn(); + $maxRow = $sheet->getHighestRow(); + $hiddenColumns = [ + 'A' => false, + 'B' => true, + 'C' => false, + 'D' => true, + 'E' => false, + 'F' => false, + 'G' => false, + 'H' => true, + 'I' => false, + 'J' => true, + 'K' => true, + 'L' => false, + ]; + foreach ($hiddenColumns as $col => $hidden) { + $sheet->getColumnDimension($col)->setVisible($hidden); + } + $sheet->getCell('D2')->setValue("=SUBTOTAL($type, A1:$maxCol$maxRow)"); + $result = $sheet->getCell('D2')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } - public function providerHiddenSUBTOTAL() + /** + * @dataProvider providerSUBTOTALHIDDEN + * + * @param mixed $expectedResult + * @param mixed $type expect an integer + */ + public function testSubtotalRowHidden($expectedResult, $type): void + { + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->fromArray([[0], [1], [1], [2], [3], [5], [8], [13], [21], [34], [55], [89]], null, 'A1', true); + $maxCol = $sheet->getHighestColumn(); + $maxRow = $sheet->getHighestRow(); + $visibleRows = [ + '1' => false, + '2' => true, + '3' => false, + '4' => true, + '5' => false, + '6' => false, + '7' => false, + '8' => true, + '9' => false, + '10' => true, + '11' => true, + '12' => false, + ]; + foreach ($visibleRows as $row => $visible) { + $sheet->getRowDimension($row)->setVisible($visible); + } + $sheet->getCell('D2')->setValue("=SUBTOTAL($type, A1:$maxCol$maxRow)"); + $result = $sheet->getCell('D2')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerSUBTOTALHIDDEN() { return require 'tests/data/Calculation/MathTrig/SUBTOTALHIDDEN.php'; } - protected function cellValues(array $cellValues) + public function testSubtotalNested(): void { - foreach ($cellValues as $k => $v) { - yield $k => $v; - } - } - - protected function cellIsFormula(array $cellValues) - { - foreach ($cellValues as $cellValue) { - yield is_string($cellValue) && $cellValue[0] === '='; - } - } - - /** - * @dataProvider providerNestedSUBTOTAL - * - * @param mixed $expectedResult - */ - public function testNestedSUBTOTAL($expectedResult, ...$args): void - { - $cellValueGenerator = $this->cellValues(Functions::flattenArray(array_slice($args, 1))); - $cellIsFormulaGenerator = $this->cellIsFormula(Functions::flattenArray(array_slice($args, 1))); - - $cell = $this->getMockBuilder(Cell::class) - ->onlyMethods(['getValue', 'isFormula']) - ->disableOriginalConstructor() - ->getMock(); - $cell->method('getValue') - ->willReturnCallback(function () use ($cellValueGenerator) { - $result = $cellValueGenerator->current(); - $cellValueGenerator->next(); - - return $result; - }); - $cell->method('isFormula') - ->willReturnCallback(function () use ($cellIsFormulaGenerator) { - $result = $cellIsFormulaGenerator->current(); - $cellIsFormulaGenerator->next(); - - return $result; - }); - $worksheet = $this->getMockBuilder(Worksheet::class) - ->onlyMethods(['cellExists', 'getCell']) - ->disableOriginalConstructor() - ->getMock(); - $worksheet->method('cellExists') - ->willReturn(true); - $worksheet->method('getCell') - ->willReturn($cell); - $cellReference = $this->getMockBuilder(Cell::class) - ->onlyMethods(['getWorksheet']) - ->disableOriginalConstructor() - ->getMock(); - $cellReference->method('getWorksheet') - ->willReturn($worksheet); - - array_push($args, $cellReference); - - $result = MathTrig::SUBTOTAL(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); - } - - public function providerNestedSUBTOTAL() - { - return require 'tests/data/Calculation/MathTrig/SUBTOTALNESTED.php'; + $sheet = $this->sheet; + $sheet->fromArray( + [ + [123], + [234], + ['=SUBTOTAL(1,A1:A2)'], + ['=ROMAN(SUBTOTAL(1, A1:A2))'], + ['This is text containing "=" and "SUBTOTAL("'], + ['=AGGREGATE(1, 0, A1:A2)'], + ['=SUM(2, 3)'], + ], + null, + 'A1', + true + ); + $maxCol = $sheet->getHighestColumn(); + $maxRow = $sheet->getHighestRow(); + $sheet->getCell('H1')->setValue("=SUBTOTAL(9, A1:$maxCol$maxRow)"); + self::assertEquals(362, $sheet->getCell('H1')->getCalculatedValue()); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfTest.php index f7ff928f..7bcd274e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfTest.php @@ -2,25 +2,36 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class SumIfTest extends TestCase +class SumIfTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUMIF * * @param mixed $expectedResult + * @param mixed $condition */ - public function testSUMIF($expectedResult, ...$args): void + public function testSUMIF2($expectedResult, array $array1, $condition, ?array $array2 = null): void { - $result = MathTrig::SUMIF(...$args); + $this->mightHaveException($expectedResult); + if ($expectedResult === 'incomplete') { + self::markTestIncomplete('Raises formula error - researching solution'); + } + $sheet = $this->sheet; + $sheet->fromArray($array1, null, 'A1', true); + $maxARow = count($array1); + $firstArg = "A1:A$maxARow"; + //$secondArg = is_string($condition) ? "\"$condition\"" : $condition; + $sheet->getCell('B1')->setValue($condition); + $secondArg = 'B1'; + if (empty($array2)) { + $sheet->getCell('D1')->setValue("=SUMIF($firstArg, $secondArg)"); + } else { + $sheet->fromArray($array2, null, 'C1', true); + $maxCRow = count($array2); + $thirdArg = "C1:C$maxCRow"; + $sheet->getCell('D1')->setValue("=SUMIF($firstArg, $secondArg, $thirdArg)"); + } + $result = $sheet->getCell('D1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumProductTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumProductTest.php index b34036e5..6e7f49e8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumProductTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumProductTest.php @@ -3,16 +3,9 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; -class SumProductTest extends TestCase +class SumProductTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUMPRODUCT * @@ -20,7 +13,24 @@ class SumProductTest extends TestCase */ public function testSUMPRODUCT($expectedResult, ...$args): void { - $result = MathTrig::SUMPRODUCT(...$args); + $sheet = $this->sheet; + $row = 0; + $arrayArg = ''; + foreach ($args as $arr) { + $arr2 = Functions::flattenArray($arr); + $startRow = 0; + foreach ($arr2 as $arr3) { + ++$row; + if (!$startRow) { + $startRow = $row; + } + $sheet->getCell("A$row")->setValue($arr3); + } + $arrayArg .= "A$startRow:A$row,"; + } + $arrayArg = substr($arrayArg, 0, -1); // strip trailing comma + $sheet->getCell('B1')->setValue("=SUMPRODUCT($arrayArg)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php new file mode 100644 index 00000000..5bd03318 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumTest.php @@ -0,0 +1,29 @@ +sheet; + $row = 0; + foreach ($args as $arg) { + ++$row; + $sheet->getCell("A$row")->setValue($arg); + } + $sheet->getCell('B1')->setValue("=SUM(A1:A$row)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerSUM() + { + return require 'tests/data/Calculation/MathTrig/SUM.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php index 5a482cd8..4db9dbb9 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class TanTest extends TestCase +class TanTest extends AllSetupTeardown { /** * @dataProvider providerTan @@ -15,11 +11,8 @@ class TanTest extends TestCase */ public function testTan($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1); $sheet->getCell('A1')->setValue("=TAN($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php index 5fe50d7c..68f87cd2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TanhTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class TanhTest extends TestCase +class TanhTest extends AllSetupTeardown { /** * @dataProvider providerTanh @@ -15,11 +11,8 @@ class TanhTest extends TestCase */ public function testTanh($expectedResult, string $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1); $sheet->getCell('A1')->setValue("=TANH($formula)"); $result = $sheet->getCell('A1')->getCalculatedValue(); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php index 37740c0d..e4127e57 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/TruncTest.php @@ -2,11 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class TruncTest extends TestCase +class TruncTest extends AllSetupTeardown { /** * @dataProvider providerTRUNC @@ -16,11 +12,8 @@ class TruncTest extends TestCase */ public function testTRUNC($expectedResult, $formula): void { - if ($expectedResult === 'exception') { - $this->expectException(CalcExp::class); - } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; $sheet->setCellValue('A2', 1.3); $sheet->setCellValue('A3', 2.7); $sheet->setCellValue('A4', -3.8); diff --git a/tests/data/Calculation/MathTrig/BASE.php b/tests/data/Calculation/MathTrig/BASE.php index c2802dd9..1f67fe2a 100644 --- a/tests/data/Calculation/MathTrig/BASE.php +++ b/tests/data/Calculation/MathTrig/BASE.php @@ -56,4 +56,10 @@ return [ 15, -1, ], + ['#VALUE!', 15, -1, '"X"'], + ['#NUM!', 15, 37], // radix > 36 + ['#NUM!', 2 ** 54, 16], // number > 2 ** 53 + ['00000120', 15, 3, 8.1], + ['exception'], + ['exception', 1], ]; diff --git a/tests/data/Calculation/MathTrig/FACT.php b/tests/data/Calculation/MathTrig/FACT.php index 331e402d..c789ced8 100644 --- a/tests/data/Calculation/MathTrig/FACT.php +++ b/tests/data/Calculation/MathTrig/FACT.php @@ -7,7 +7,7 @@ return [ ], [ 1, - 1.8999999999999999, + 1.9, ], [ 1, @@ -35,7 +35,7 @@ return [ ], [ 6, - 3.2000000000000002, + 3.2, ], [ '#VALUE!', diff --git a/tests/data/Calculation/MathTrig/FACTGNUMERIC.php b/tests/data/Calculation/MathTrig/FACTGNUMERIC.php new file mode 100644 index 00000000..0f040dd0 --- /dev/null +++ b/tests/data/Calculation/MathTrig/FACTGNUMERIC.php @@ -0,0 +1,44 @@ + ['A' => 0], - 2 => ['A' => 1], - 3 => ['A' => 1], - 4 => ['A' => 2], - 5 => ['A' => 3], - 6 => ['A' => 5], - 7 => ['A' => 8], - 8 => ['A' => 13], - 9 => ['A' => 21], - 10 => ['A' => 34], - 11 => ['A' => 55], - 12 => ['A' => 89], -]; - return [ - [ - 19.3333333333333, - 1, - $baseTestData, - ], - [ - 12, - 2, - $baseTestData, - ], - [ - 12, - 3, - $baseTestData, - ], - [ - 89, - 4, - $baseTestData, - ], - [ - 0, - 5, - $baseTestData, - ], - [ - 0, - 6, - $baseTestData, - ], - [ - 27.5196899207337, - 7, - $baseTestData, - ], - [ - 26.3480971271593, - 8, - $baseTestData, - ], - [ - 232, - 9, - $baseTestData, - ], - [ - 757.3333333333330, - 10, - $baseTestData, - ], - [ - 694.2222222222220, - 11, - $baseTestData, - ], + [19.3333333333333, 1], + [12, 2], + [12, 3], + [89, 4], + [0, 5], + [0, 6], + [27.5196899207337, 7], + [26.3480971271593, 8], + [232, 9], + [757.3333333333330, '10'], + [694.2222222222220, 11.1], + ['#VALUE!', 0], + ['#VALUE!', -1], + ['#VALUE!', 12], + ['#VALUE!', '"X"'], ]; diff --git a/tests/data/Calculation/MathTrig/SUBTOTALHIDDEN.php b/tests/data/Calculation/MathTrig/SUBTOTALHIDDEN.php index accaf03e..df6375dc 100644 --- a/tests/data/Calculation/MathTrig/SUBTOTALHIDDEN.php +++ b/tests/data/Calculation/MathTrig/SUBTOTALHIDDEN.php @@ -1,100 +1,15 @@ ['A' => 0], - 2 => ['A' => 1], - 3 => ['A' => 1], - 4 => ['A' => 2], - 5 => ['A' => 3], - 6 => ['A' => 5], - 7 => ['A' => 8], - 8 => ['A' => 13], - 9 => ['A' => 21], - 10 => ['A' => 34], - 11 => ['A' => 55], - 12 => ['A' => 89], -]; - -$hiddenRows = [ - 1 => false, - 2 => true, - 3 => false, - 4 => true, - 5 => false, - 6 => false, - 7 => false, - 8 => true, - 9 => false, - 10 => true, - 11 => true, - 12 => false, -]; - return [ - [ - 21, - $hiddenRows, - 101, - $baseTestData, - ], - [ - 5, - $hiddenRows, - 102, - $baseTestData, - ], - [ - 5, - $hiddenRows, - 103, - $baseTestData, - ], - [ - 55, - $hiddenRows, - 104, - $baseTestData, - ], - [ - 1, - $hiddenRows, - 105, - $baseTestData, - ], - [ - 48620, - $hiddenRows, - 106, - $baseTestData, - ], - [ - 23.1840462387393, - $hiddenRows, - 107, - $baseTestData, - ], - [ - 20.7364413533277, - $hiddenRows, - 108, - $baseTestData, - ], - [ - 105, - $hiddenRows, - 109, - $baseTestData, - ], - [ - 537.5, - $hiddenRows, - 110, - $baseTestData, - ], - [ - 430, - $hiddenRows, - 111, - $baseTestData, - ], + [21, 101], + [5, 102], + [5, 103], + [55, 104], + [1, 105], + [48620, 106], + [23.1840462387393, 107], + [20.7364413533277, 108], + [105, 109], + [537.5, 110], + [430, 111], ]; diff --git a/tests/data/Calculation/MathTrig/SUBTOTALNESTED.php b/tests/data/Calculation/MathTrig/SUBTOTALNESTED.php deleted file mode 100644 index e1ae38f8..00000000 --- a/tests/data/Calculation/MathTrig/SUBTOTALNESTED.php +++ /dev/null @@ -1,18 +0,0 @@ - ['A' => 123], - 2 => ['A' => 234], - 3 => ['A' => '=SUBTOTAL(1, A1:A2)'], - 4 => ['A' => '=ROMAN(SUBTOTAL(1, A1:A2))'], - 5 => ['A' => 'This is text containing "=" and "SUBTOTAL("'], - 6 => ['A' => '=AGGREGATE(1, A1:A2)'], -]; - -return [ - [ - 357, - 9, - $baseTestData, - ], -]; diff --git a/tests/data/Calculation/MathTrig/SUM.php b/tests/data/Calculation/MathTrig/SUM.php new file mode 100644 index 00000000..a8219076 --- /dev/null +++ b/tests/data/Calculation/MathTrig/SUM.php @@ -0,0 +1,8 @@ + Date: Fri, 26 Mar 2021 18:29:05 +0100 Subject: [PATCH 64/89] Switch calls to deprecated function methods to the equivalent new methods (#1957) --- .../Calculation/Database/DProduct.php | 2 +- .../Calculation/Database/DSum.php | 2 +- .../Calculation/Financial/Amortization.php | 10 ++-- .../Calculation/Financial/Coupons.php | 23 ++++----- .../Calculation/Financial/Helpers.php | 4 +- .../Financial/Securities/BaseValidations.php | 4 +- .../Financial/Securities/Price.php | 12 ++--- .../Financial/Securities/Yields.php | 14 +++--- .../Calculation/Financial/TreasuryBill.php | 49 +++++++++---------- .../Calculation/MathTrig/Fact.php | 2 +- .../Calculation/Statistical.php | 2 +- .../Statistical/Distributions/Poisson.php | 4 +- .../Calculation/Statistical/Permutations.php | 2 +- .../Calculation/TextData/Format.php | 8 +-- .../Calculation/TextData/Replace.php | 3 +- src/PhpSpreadsheet/Shared/Date.php | 19 +++---- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 4 +- 17 files changed, 82 insertions(+), 82 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Database/DProduct.php b/src/PhpSpreadsheet/Calculation/Database/DProduct.php index 107c69c0..f02eb196 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DProduct.php +++ b/src/PhpSpreadsheet/Calculation/Database/DProduct.php @@ -38,7 +38,7 @@ class DProduct extends DatabaseAbstract return null; } - return MathTrig::PRODUCT( + return MathTrig\Product::funcProduct( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Database/DSum.php b/src/PhpSpreadsheet/Calculation/Database/DSum.php index 473bacd1..4f784e19 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DSum.php +++ b/src/PhpSpreadsheet/Calculation/Database/DSum.php @@ -38,7 +38,7 @@ class DSum extends DatabaseAbstract return null; } - return MathTrig::SUM( + return MathTrig\Sum::funcSum( self::getFilteredColumn($database, $field, $criteria) ); } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php index 7bb7fb40..f1a9e3f5 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Amortization @@ -47,7 +47,7 @@ class Amortization $rate = Functions::flattenSingleValue($rate); $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); - $yearFrac = DateTime::YEARFRAC($purchased, $firstPeriod, $basis); + $yearFrac = DateTimeExcel\YearFrac::funcYearFrac($purchased, $firstPeriod, $basis); if (is_string($yearFrac)) { return $yearFrac; } @@ -116,13 +116,13 @@ class Amortization $fOneRate = $cost * $rate; $fCostDelta = $cost - $salvage; // Note, quirky variation for leap years on the YEARFRAC for this function - $purchasedYear = DateTime::YEAR($purchased); - $yearFrac = DateTime::YEARFRAC($purchased, $firstPeriod, $basis); + $purchasedYear = DateTimeExcel\Year::funcYear($purchased); + $yearFrac = DateTimeExcel\YearFrac::funcYearFrac($purchased, $firstPeriod, $basis); if (is_string($yearFrac)) { return $yearFrac; } - if (($basis == 1) && ($yearFrac < 1) && (DateTime::isLeapYear($purchasedYear))) { + if (($basis == 1) && ($yearFrac < 1) && (DateTimeExcel\Helpers::isLeapYear($purchasedYear))) { $yearFrac *= 365 / 366; } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index d0efd689..c4a60d90 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -2,7 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -60,14 +61,14 @@ class Coupons return $e->getMessage(); } - $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis); $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); if ($basis === Helpers::DAYS_PER_YEAR_ACTUAL) { - return abs(DateTime::DAYS($prev, $settlement)); + return abs(DateTimeExcel\Days::funcDays($prev, $settlement)); } - return DateTime::YEARFRAC($prev, $settlement, $basis) * $daysPerYear; + return DateTimeExcel\YearFrac::funcYearFrac($prev, $settlement, $basis) * $daysPerYear; } /** @@ -121,7 +122,7 @@ class Coupons case Helpers::DAYS_PER_YEAR_ACTUAL: // Actual/actual if ($frequency == self::FREQUENCY_ANNUAL) { - $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis); return $daysPerYear / $frequency; } @@ -179,7 +180,7 @@ class Coupons return $e->getMessage(); } - $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis); $next = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_NEXT); if ($basis === Helpers::DAYS_PER_YEAR_NASD) { @@ -190,7 +191,7 @@ class Coupons } } - return DateTime::YEARFRAC($settlement, $next, $basis) * $daysPerYear; + return DateTimeExcel\YearFrac::funcYearFrac($settlement, $next, $basis) * $daysPerYear; } /** @@ -286,7 +287,7 @@ class Coupons return $e->getMessage(); } - $yearsBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, 0); + $yearsBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, 0); return ceil($yearsBetweenSettlementAndMaturity * $frequency); } @@ -344,11 +345,11 @@ class Coupons * * Returns a boolean TRUE/FALSE indicating if this date is the last date of the month * - * @param \DateTime $testDate The date for testing + * @param DateTime $testDate The date for testing * * @return bool */ - private static function isLastDayOfMonth(\DateTime $testDate) + private static function isLastDayOfMonth(DateTime $testDate) { return $testDate->format('d') === $testDate->format('t'); } @@ -376,7 +377,7 @@ class Coupons private static function validateInputDate($date) { - $date = DateTime::getDateValue($date); + $date = DateTimeExcel\Helpers::getDateValue($date); if (is_string($date)) { throw new Exception(Functions::VALUE()); } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Helpers.php b/src/PhpSpreadsheet/Calculation/Financial/Helpers.php index 0b8d97b1..08c942ab 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Helpers.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Helpers @@ -42,7 +42,7 @@ class Helpers case self::DAYS_PER_YEAR_365: return 365; case self::DAYS_PER_YEAR_ACTUAL: - return (DateTime::isLeapYear($year)) ? 366 : 365; + return (DateTimeExcel\Helpers::isLeapYear($year)) ? 366 : 365; } return Functions::NAN(); diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php index 88cb8660..2a5e5dd2 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities\Constants as SecuritiesConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -11,7 +11,7 @@ abstract class BaseValidations { protected static function validateInputDate($date) { - $date = DateTime::getDateValue($date); + $date = DateTimeExcel\Helpers::getDateValue($date); if (is_string($date)) { throw new Exception(Functions::VALUE()); } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php index 14be7f84..18a0a2e1 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; @@ -117,7 +117,7 @@ class Price extends BaseValidations return $e->getMessage(); } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -169,23 +169,23 @@ class Price extends BaseValidations return $e->getMessage(); } - $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis); if (!is_numeric($daysPerYear)) { return $daysPerYear; } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = DateTimeExcel\YearFrac::funcYearFrac($issue, $settlement, $basis); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; } $daysBetweenIssueAndSettlement *= $daysPerYear; - $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); + $daysBetweenIssueAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($issue, $maturity, $basis); if (!is_numeric($daysBetweenIssueAndMaturity)) { // return date error return $daysBetweenIssueAndMaturity; } $daysBetweenIssueAndMaturity *= $daysPerYear; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php index 0918d637..86151904 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -49,11 +49,11 @@ class Yields extends BaseValidations return $e->getMessage(); } - $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis); if (!is_numeric($daysPerYear)) { return $daysPerYear; } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -106,23 +106,23 @@ class Yields extends BaseValidations return $e->getMessage(); } - $daysPerYear = Helpers::daysPerYear(DateTime::YEAR($settlement), $basis); + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis); if (!is_numeric($daysPerYear)) { return $daysPerYear; } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = DateTimeExcel\YearFrac::funcYearFrac($issue, $settlement, $basis); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; } $daysBetweenIssueAndSettlement *= $daysPerYear; - $daysBetweenIssueAndMaturity = DateTime::YEARFRAC($issue, $maturity, $basis); + $daysBetweenIssueAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($issue, $maturity, $basis); if (!is_numeric($daysBetweenIssueAndMaturity)) { // return date error return $daysBetweenIssueAndMaturity; } $daysBetweenIssueAndMaturity *= $daysPerYear; - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; diff --git a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php index 3177124a..966500bf 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php +++ b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php @@ -2,7 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class TreasuryBill @@ -27,11 +28,11 @@ class TreasuryBill $maturity = Functions::flattenSingleValue($maturity); $discount = Functions::flattenSingleValue($discount); - if ( - is_string($maturity = DateTime::getDateValue($maturity)) || - is_string($settlement = DateTime::getDateValue($settlement)) - ) { - return Functions::VALUE(); + try { + $maturity = DateTimeExcel\Helpers::getDateValue($maturity); + $settlement = DateTimeExcel\Helpers::getDateValue($settlement); + } catch (Exception $e) { + return $e->getMessage(); } // Validate @@ -41,11 +42,9 @@ class TreasuryBill } $daysBetweenSettlementAndMaturity = $maturity - $settlement; + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($maturity), Helpers::DAYS_PER_YEAR_ACTUAL); - if ( - $daysBetweenSettlementAndMaturity > Helpers::daysPerYear(DateTime::YEAR($maturity), Helpers::DAYS_PER_YEAR_ACTUAL) || - $daysBetweenSettlementAndMaturity < 0 - ) { + if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { return Functions::NAN(); } @@ -75,11 +74,11 @@ class TreasuryBill $maturity = Functions::flattenSingleValue($maturity); $discount = Functions::flattenSingleValue($discount); - if ( - is_string($maturity = DateTime::getDateValue($maturity)) || - is_string($settlement = DateTime::getDateValue($settlement)) - ) { - return Functions::VALUE(); + try { + $maturity = DateTimeExcel\Helpers::getDateValue($maturity); + $settlement = DateTimeExcel\Helpers::getDateValue($settlement); + } catch (Exception $e) { + return $e->getMessage(); } // Validate @@ -89,13 +88,12 @@ class TreasuryBill } $daysBetweenSettlementAndMaturity = $maturity - $settlement; + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($maturity), Helpers::DAYS_PER_YEAR_ACTUAL); - if ( - $daysBetweenSettlementAndMaturity > Helpers::daysPerYear(DateTime::YEAR($maturity), Helpers::DAYS_PER_YEAR_ACTUAL) || - $daysBetweenSettlementAndMaturity < 0 - ) { + if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { return Functions::NAN(); } + $price = 100 * (1 - (($discount * $daysBetweenSettlementAndMaturity) / 360)); if ($price < 0.0) { return Functions::NAN(); @@ -127,11 +125,11 @@ class TreasuryBill $maturity = Functions::flattenSingleValue($maturity); $price = Functions::flattenSingleValue($price); - if ( - is_string($maturity = DateTime::getDateValue($maturity)) || - is_string($settlement = DateTime::getDateValue($settlement)) - ) { - return Functions::VALUE(); + try { + $maturity = DateTimeExcel\Helpers::getDateValue($maturity); + $settlement = DateTimeExcel\Helpers::getDateValue($settlement); + } catch (Exception $e) { + return $e->getMessage(); } // Validate @@ -141,8 +139,9 @@ class TreasuryBill } $daysBetweenSettlementAndMaturity = $maturity - $settlement; + $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($maturity), Helpers::DAYS_PER_YEAR_ACTUAL); - if ($daysBetweenSettlementAndMaturity > 360 || $daysBetweenSettlementAndMaturity < 0) { + if ($daysBetweenSettlementAndMaturity > $daysPerYear || $daysBetweenSettlementAndMaturity < 0) { return Functions::NAN(); } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Fact.php b/src/PhpSpreadsheet/Calculation/MathTrig/Fact.php index 0d591b77..026cb9a2 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Fact.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Fact.php @@ -33,7 +33,7 @@ class Fact $factLoop = floor($factVal); if ($factVal > $factLoop) { if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - return Statistical::GAMMAFunction($factVal + 1); + return Statistical\Distributions\Gamma::gammaValue($factVal + 1); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index ca160c36..695d7cbd 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -946,7 +946,7 @@ class Statistical { $aArgs = Functions::flattenArray($args); - $aMean = MathTrig::PRODUCT($aArgs); + $aMean = MathTrig\Product::funcProduct($aArgs); if (is_numeric($aMean) && ($aMean > 0)) { $aCount = Counts::COUNT($aArgs); if (Minimum::MIN($aArgs) > 0) { diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php index 1ba7adca..51d097b3 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php @@ -44,12 +44,12 @@ class Poisson $summer = 0; $floor = floor($value); for ($i = 0; $i <= $floor; ++$i) { - $summer += $mean ** $i / MathTrig::FACT($i); + $summer += $mean ** $i / MathTrig\Fact::funcFact($i); } return exp(0 - $mean) * $summer; } - return (exp(0 - $mean) * $mean ** $value) / MathTrig::FACT($value); + return (exp(0 - $mean) * $mean ** $value) / MathTrig\Fact::funcFact($value); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php index 5d03e5d5..343a056c 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -32,7 +32,7 @@ class Permutations return Functions::NAN(); } - return round(MathTrig::FACT($numObjs) / MathTrig::FACT($numObjs - $numInSet)); + return round(MathTrig\Fact::funcFact($numObjs) / MathTrig\Fact::funcFact($numObjs - $numInSet)); } return Functions::VALUE(); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index f24ed7ae..c061818e 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use DateTimeInterface; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use PhpOffice\PhpSpreadsheet\Shared\Date; @@ -96,7 +96,7 @@ class Format $format = Functions::flattenSingleValue($format); if ((is_string($value)) && (!is_numeric($value)) && Date::isDateTimeFormatCode($format)) { - $value = DateTime::DATEVALUE($value); + $value = DateTimeExcel\DateValue::funcDateValue($value); } return (string) NumberFormat::toFormattedString($value, $format); @@ -127,14 +127,14 @@ class Format Functions::setReturnDateType(Functions::RETURNDATE_EXCEL); if (strpos($value, ':') !== false) { - $timeValue = DateTime::TIMEVALUE($value); + $timeValue = DateTimeExcel\TimeValue::funcTimeValue($value); if ($timeValue !== Functions::VALUE()) { Functions::setReturnDateType($dateSetting); return $timeValue; } } - $dateValue = DateTime::DATEVALUE($value); + $dateValue = DateTimeExcel\DateValue::funcDateValue($value); if ($dateValue !== Functions::VALUE()) { Functions::setReturnDateType($dateSetting); diff --git a/src/PhpSpreadsheet/Calculation/TextData/Replace.php b/src/PhpSpreadsheet/Calculation/TextData/Replace.php index a06d4364..a1975d6b 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Replace.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Replace.php @@ -3,7 +3,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\TextData; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\TextData; class Replace { @@ -23,7 +22,7 @@ class Replace $newText = Functions::flattenSingleValue($newText); $left = Extract::left($oldText, $start - 1); - $right = Extract::right($oldText, TextData::STRINGLENGTH($oldText) - ($start + $chars) + 1); + $right = Extract::right($oldText, Text::length($oldText) - ($start + $chars) + 1); return $left . $newText . $right; } diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index 28c39255..a6cacb6f 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -2,9 +2,10 @@ namespace PhpOffice\PhpSpreadsheet\Shared; +use DateTime; use DateTimeInterface; use DateTimeZone; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; @@ -154,7 +155,7 @@ class Date * if you don't want to treat it as a UTC value * Use the default (UST) unless you absolutely need a conversion * - * @return \DateTime PHP date/time object + * @return DateTime PHP date/time object */ public static function excelToDateTimeObject($excelTimestamp, $timeZone = null) { @@ -162,18 +163,18 @@ class Date if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_EXCEL) { if ($excelTimestamp < 1 && self::$excelCalendar === self::CALENDAR_WINDOWS_1900) { // Unix timestamp base date - $baseDate = new \DateTime('1970-01-01', $timeZone); + $baseDate = new DateTime('1970-01-01', $timeZone); } else { // MS Excel calendar base dates if (self::$excelCalendar == self::CALENDAR_WINDOWS_1900) { // Allow adjustment for 1900 Leap Year in MS Excel - $baseDate = ($excelTimestamp < 60) ? new \DateTime('1899-12-31', $timeZone) : new \DateTime('1899-12-30', $timeZone); + $baseDate = ($excelTimestamp < 60) ? new DateTime('1899-12-31', $timeZone) : new DateTime('1899-12-30', $timeZone); } else { - $baseDate = new \DateTime('1904-01-01', $timeZone); + $baseDate = new DateTime('1904-01-01', $timeZone); } } } else { - $baseDate = new \DateTime('1899-12-30', $timeZone); + $baseDate = new DateTime('1899-12-30', $timeZone); } $days = floor($excelTimestamp); @@ -262,7 +263,7 @@ class Date return false; } - return self::dateTimeToExcel(new \DateTime('@' . $dateValue)); + return self::dateTimeToExcel(new DateTime('@' . $dateValue)); } /** @@ -436,14 +437,14 @@ class Date return false; } - $dateValueNew = DateTime::DATEVALUE($dateValue); + $dateValueNew = DateTimeExcel\DateValue::funcDateValue($dateValue); if ($dateValueNew === Functions::VALUE()) { return false; } if (strpos($dateValue, ':') !== false) { - $timeValue = DateTime::TIMEVALUE($dateValue); + $timeValue = DateTimeExcel\TimeValue::funcTimeValue($dateValue); if ($timeValue === Functions::VALUE()) { return false; } diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 4c33eb37..22fc775c 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -3,7 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; @@ -472,7 +472,7 @@ class AutoFilter $val = $maxVal = null; $ruleValues = []; - $baseDate = DateTime::DATENOW(); + $baseDate = DateTimeExcel\Now::funcNow(); // Calculate start/end dates for the required date range based on current date switch ($dynamicRuleType) { case AutoFilter\Column\Rule::AUTOFILTER_RULETYPE_DYNAMIC_LASTWEEK: From c699d144e20967afa83cee17d6dbf01192b614eb Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Fri, 26 Mar 2021 22:49:16 +0100 Subject: [PATCH 65/89] Extract ACCRINT() and ACCRINTM() Financial functions into their own class (#1956) * Extract ACCRINT() and ACCRINTM() Financial functions into their own class Implement additional validations, with additional unit tests Add support for the new calculation method argument for ACCRINT() * Additional tests for Amortization functions --- .../Calculation/Calculation.php | 6 +- .../Engineering/BaseValidations.php | 27 ++++ .../Calculation/Engineering/BesselI.php | 25 +-- .../Calculation/Engineering/BesselJ.php | 25 +-- .../Calculation/Engineering/BesselK.php | 30 ++-- .../Calculation/Engineering/BesselY.php | 30 ++-- .../Calculation/Engineering/Compare.php | 17 ++- .../Calculation/Engineering/Complex.php | 15 +- .../Calculation/Engineering/Erf.php | 6 +- src/PhpSpreadsheet/Calculation/Financial.php | 126 +++++++-------- .../Calculation/Financial/Amortization.php | 27 ++++ .../Calculation/Financial/BaseValidations.php | 72 +++++++++ .../Calculation/Financial/Coupons.php | 76 +--------- .../Calculation/Financial/Depreciation.php | 38 +---- .../Calculation/Financial/Helpers.php | 15 ++ .../Calculation/Financial/InterestRate.php | 25 +-- .../Financial/Securities/AccruedInterest.php | 143 ++++++++++++++++++ .../Financial/Securities/BaseValidations.php | 58 ++++--- .../Financial/Securities/Price.php | 4 +- .../Financial/Securities/Yields.php | 4 +- .../Calculation/Financial/TreasuryBill.php | 14 +- .../Functions/Financial/AccrintMTest.php | 2 +- .../Functions/Financial/AccrintTest.php | 2 +- tests/data/Calculation/Financial/ACCRINT.php | 113 +++++++------- tests/data/Calculation/Financial/ACCRINTM.php | 59 +++++--- .../data/Calculation/Financial/AMORDEGRC.php | 12 ++ .../data/Calculation/Financial/COUPDAYBS.php | 4 +- tests/data/Calculation/Financial/COUPDAYS.php | 4 +- .../data/Calculation/Financial/COUPDAYSNC.php | 4 +- tests/data/Calculation/Financial/COUPNCD.php | 4 +- tests/data/Calculation/Financial/COUPNUM.php | 4 +- tests/data/Calculation/Financial/COUPPCD.php | 4 +- 32 files changed, 625 insertions(+), 370 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Engineering/BaseValidations.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/BaseValidations.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 3cce499f..e1ccb74b 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -233,12 +233,12 @@ class Calculation ], 'ACCRINT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'ACCRINT'], - 'argumentCount' => '4-7', + 'functionCall' => [Financial\Securities\AccruedInterest::class, 'periodic'], + 'argumentCount' => '4-8', ], 'ACCRINTM' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'ACCRINTM'], + 'functionCall' => [Financial\Securities\AccruedInterest::class, 'atMaturity'], 'argumentCount' => '3-5', ], 'ACOS' => [ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Engineering/BaseValidations.php new file mode 100644 index 00000000..48317635 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Engineering/BaseValidations.php @@ -0,0 +1,27 @@ +getMessage(); } - return Functions::VALUE(); + if ($ord < 0) { + return Functions::NAN(); + } + + $fResult = self::calculate($x, $ord); + + return (is_nan($fResult)) ? Functions::NAN() : $fResult; } private static function calculate(float $x, int $ord): float diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php index 5e8bfbf5..ca9ff4f7 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class BesselJ { + use BaseValidations; + /** * BESSELJ. * @@ -30,18 +33,20 @@ class BesselJ $x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x); $ord = ($ord === null) ? 0.0 : Functions::flattenSingleValue($ord); - if ((is_numeric($x)) && (is_numeric($ord))) { - $ord = (int) floor($ord); - if ($ord < 0) { - return Functions::NAN(); - } - - $fResult = self::calculate((float) $x, $ord); - - return (is_nan($fResult)) ? Functions::NAN() : $fResult; + try { + $x = self::validateFloat($x); + $ord = self::validateInt($ord); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if ($ord < 0) { + return Functions::NAN(); + } + + $fResult = self::calculate($x, $ord); + + return (is_nan($fResult)) ? Functions::NAN() : $fResult; } private static function calculate(float $x, int $ord): float diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php index ff32b78a..faba191f 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class BesselK { + use BaseValidations; + /** * BESSELK. * @@ -28,25 +31,26 @@ class BesselK $x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x); $ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord); - if ((is_numeric($x)) && (is_numeric($ord))) { - $ord = (int) floor($ord); - $x = (float) $x; - if (($ord < 0) || ($x <= 0.0)) { - return Functions::NAN(); - } - - $fBk = self::calculate($x, $ord); - - return (is_nan($fBk)) ? Functions::NAN() : $fBk; + try { + $x = self::validateFloat($x); + $ord = self::validateInt($ord); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if (($ord < 0) || ($x <= 0.0)) { + return Functions::NAN(); + } + + $fBk = self::calculate($x, $ord); + + return (is_nan($fBk)) ? Functions::NAN() : $fBk; } - private static function calculate($x, $ord): float + private static function calculate(float $x, int $ord): float { // special cases - switch (floor($ord)) { + switch ($ord) { case 0: return self::besselK0($x); case 1: diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php index 09694381..1eed5a54 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class BesselY { + use BaseValidations; + /** * BESSELY. * @@ -27,25 +30,26 @@ class BesselY $x = ($x === null) ? 0.0 : Functions::flattenSingleValue($x); $ord = ($ord === null) ? 0 : Functions::flattenSingleValue($ord); - if ((is_numeric($x)) && (is_numeric($ord))) { - $ord = (int) floor($ord); - $x = (float) $x; - if (($ord < 0) || ($x <= 0.0)) { - return Functions::NAN(); - } - - $fBy = self::calculate($x, $ord); - - return (is_nan($fBy)) ? Functions::NAN() : $fBy; + try { + $x = self::validateFloat($x); + $ord = self::validateInt($ord); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if (($ord < 0) || ($x <= 0.0)) { + return Functions::NAN(); + } + + $fBy = self::calculate($x, $ord); + + return (is_nan($fBy)) ? Functions::NAN() : $fBy; } - private static function calculate($x, $ord): float + private static function calculate(float $x, int $ord): float { // special cases - switch (floor($ord)) { + switch ($ord) { case 0: return self::besselY0($x); case 1: diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php index d875174e..c764d8ea 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Compare.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Compare.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Compare { + use BaseValidations; + /** * DELTA. * @@ -27,8 +30,11 @@ class Compare $a = Functions::flattenSingleValue($a); $b = Functions::flattenSingleValue($b); - if (!is_numeric($a) || !is_numeric($b)) { - return Functions::VALUE(); + try { + $a = self::validateFloat($a); + $b = self::validateFloat($b); + } catch (Exception $e) { + return $e->getMessage(); } return (int) ($a == $b); @@ -54,8 +60,11 @@ class Compare $number = Functions::flattenSingleValue($number); $step = Functions::flattenSingleValue($step); - if (!is_numeric($number) || !is_numeric($step)) { - return Functions::VALUE(); + try { + $number = self::validateFloat($number); + $step = self::validateFloat($step); + } catch (Exception $e) { + return $e->getMessage(); } return (int) ($number >= $step); diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Complex.php b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php index 7dd5ff95..a1a64768 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Complex.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php @@ -4,10 +4,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Engineering; use Complex\Complex as ComplexObject; use Complex\Exception as ComplexException; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Complex { + use BaseValidations; + /** * COMPLEX. * @@ -29,10 +32,14 @@ class Complex $imaginary = ($imaginary === null) ? 0.0 : Functions::flattenSingleValue($imaginary); $suffix = ($suffix === null) ? 'i' : Functions::flattenSingleValue($suffix); - if ( - ((is_numeric($realNumber)) && (is_numeric($imaginary))) && - (($suffix == 'i') || ($suffix == 'j') || ($suffix == '')) - ) { + try { + $realNumber = self::validateFloat($realNumber); + $imaginary = self::validateFloat($imaginary); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($suffix == 'i') || ($suffix == 'j') || ($suffix == '')) { $complex = new ComplexObject($realNumber, $imaginary, $suffix); return (string) $complex; diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Erf.php b/src/PhpSpreadsheet/Calculation/Engineering/Erf.php index 54358ebd..a5df425e 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Erf.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Erf.php @@ -21,8 +21,8 @@ class Erf * Excel Function: * ERF(lower[,upper]) * - * @param float $lower lower bound for integrating ERF - * @param float $upper upper bound for integrating ERF. + * @param mixed (float) $lower lower bound for integrating ERF + * @param mixed (float) $upper upper bound for integrating ERF. * If omitted, ERF integrates between zero and lower_limit * * @return float|string @@ -52,7 +52,7 @@ class Erf * Excel Function: * ERF.PRECISE(limit) * - * @param float $limit bound for integrating ERF + * @param mixed (float) $limit bound for integrating ERF * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 084562f8..1a67ef33 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -35,57 +35,58 @@ class Financial * Returns the accrued interest for a security that pays periodic interest. * * Excel Function: - * ACCRINT(issue,firstinterest,settlement,rate,par,frequency[,basis]) + * ACCRINT(issue,firstinterest,settlement,rate,par,frequency[,basis][,calc_method]) + * + * @Deprecated 1.18.0 + * + * @see Financial\Securities\AccruedInterest::periodic() + * Use the periodic() method in the Financial\Securities\AccruedInterest class instead * * @param mixed $issue the security's issue date * @param mixed $firstinterest the security's first interest date * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue date - * when the security is traded to the buyer. + * The security settlement date is the date after the issue date + * when the security is traded to the buyer. * @param mixed (float) $rate the security's annual coupon rate * @param mixed (float) $par The security's par value. - * If you omit par, ACCRINT uses $1,000. - * @param mixed (int) $frequency the number of coupon payments per year. + * If you omit par, ACCRINT uses $1,000. + * @param mixed (int) $frequency The number of coupon payments per year. * Valid frequency values are: * 1 Annual * 2 Semi-Annual * 4 Quarterly * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * @param mixed (bool) $calcMethod + * If true, use Issue to Settlement + * If false, use FirstInterest to Settlement * * @return float|string Result, or a string containing an error */ - public static function ACCRINT($issue, $firstinterest, $settlement, $rate, $par = 1000, $frequency = 1, $basis = 0) - { - $issue = Functions::flattenSingleValue($issue); - $firstinterest = Functions::flattenSingleValue($firstinterest); - $settlement = Functions::flattenSingleValue($settlement); - $rate = Functions::flattenSingleValue($rate); - $par = ($par === null) ? 1000 : Functions::flattenSingleValue($par); - $frequency = ($frequency === null) ? 1 : Functions::flattenSingleValue($frequency); - $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); - - // Validate - if ((is_numeric($rate)) && (is_numeric($par))) { - $rate = (float) $rate; - $par = (float) $par; - if (($rate <= 0) || ($par <= 0)) { - return Functions::NAN(); - } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); - if (!is_numeric($daysBetweenIssueAndSettlement)) { - // return date error - return $daysBetweenIssueAndSettlement; - } - - return $par * $rate * $daysBetweenIssueAndSettlement; - } - - return Functions::VALUE(); + public static function ACCRINT( + $issue, + $firstinterest, + $settlement, + $rate, + $par = 1000, + $frequency = 1, + $basis = 0, + $calcMethod = true + ) { + return Securities\AccruedInterest::periodic( + $issue, + $firstinterest, + $settlement, + $rate, + $par, + $frequency, + $basis, + $calcMethod + ); } /** @@ -96,45 +97,28 @@ class Financial * Excel Function: * ACCRINTM(issue,settlement,rate[,par[,basis]]) * + * @Deprecated 1.18.0 + * + * @see Financial\Securities\AccruedInterest::atMaturity() + * Use the atMaturity() method in the Financial\Securities\AccruedInterest class instead + * * @param mixed $issue The security's issue date * @param mixed $settlement The security's settlement (or maturity) date * @param mixed (float) $rate The security's annual coupon rate * @param mixed (float) $par The security's par value. - * If you omit par, ACCRINT uses $1,000. + * If you omit par, ACCRINT uses $1,000. * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string Result, or a string containing an error */ public static function ACCRINTM($issue, $settlement, $rate, $par = 1000, $basis = 0) { - $issue = Functions::flattenSingleValue($issue); - $settlement = Functions::flattenSingleValue($settlement); - $rate = Functions::flattenSingleValue($rate); - $par = ($par === null) ? 1000 : Functions::flattenSingleValue($par); - $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); - - // Validate - if ((is_numeric($rate)) && (is_numeric($par))) { - $rate = (float) $rate; - $par = (float) $par; - if (($rate <= 0) || ($par <= 0)) { - return Functions::NAN(); - } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); - if (!is_numeric($daysBetweenIssueAndSettlement)) { - // return date error - return $daysBetweenIssueAndSettlement; - } - - return $par * $rate * $daysBetweenIssueAndSettlement; - } - - return Functions::VALUE(); + return Securities\AccruedInterest::atMaturity($issue, $settlement, $rate, $par, $basis); } /** @@ -163,11 +147,11 @@ class Financial * @param float $period The period * @param float $rate Rate of depreciation * @param int $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string (string containing the error type if there is an error) */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php index f1a9e3f5..9e838a26 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php @@ -3,10 +3,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Amortization { + use BaseValidations; + /** * AMORDEGRC. * @@ -47,6 +50,18 @@ class Amortization $rate = Functions::flattenSingleValue($rate); $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); + try { + $cost = self::validateFloat($cost); + $purchased = self::validateDate($purchased); + $firstPeriod = self::validateDate($firstPeriod); + $salvage = self::validateFloat($salvage); + $period = self::validateFloat($period); + $rate = self::validateFloat($rate); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + $yearFrac = DateTimeExcel\YearFrac::funcYearFrac($purchased, $firstPeriod, $basis); if (is_string($yearFrac)) { return $yearFrac; @@ -113,6 +128,18 @@ class Amortization $rate = Functions::flattenSingleValue($rate); $basis = ($basis === null) ? 0 : (int) Functions::flattenSingleValue($basis); + try { + $cost = self::validateFloat($cost); + $purchased = self::validateDate($purchased); + $firstPeriod = self::validateDate($firstPeriod); + $salvage = self::validateFloat($salvage); + $period = self::validateFloat($period); + $rate = self::validateFloat($rate); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + $fOneRate = $cost * $rate; $fCostDelta = $cost - $salvage; // Note, quirky variation for leap years on the YEARFRAC for this function diff --git a/src/PhpSpreadsheet/Calculation/Financial/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Financial/BaseValidations.php new file mode 100644 index 00000000..01d9ab30 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/BaseValidations.php @@ -0,0 +1,72 @@ + 4)) { + throw new Exception(Functions::NAN()); + } + + return $basis; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index c4a60d90..ce83ccb4 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -2,7 +2,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; -use DateTime; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -10,6 +9,8 @@ use PhpOffice\PhpSpreadsheet\Shared\Date; class Coupons { + use BaseValidations; + public const FREQUENCY_ANNUAL = 1; public const FREQUENCY_SEMI_ANNUAL = 2; public const FREQUENCY_QUARTERLY = 4; @@ -62,6 +63,9 @@ class Coupons } $daysPerYear = Helpers::daysPerYear(DateTimeExcel\Year::funcYear($settlement), $basis); + if (is_string($daysPerYear)) { + return Functions::VALUE(); + } $prev = self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); if ($basis === Helpers::DAYS_PER_YEAR_ACTUAL) { @@ -185,7 +189,7 @@ class Coupons if ($basis === Helpers::DAYS_PER_YEAR_NASD) { $settlementDate = Date::excelToDateTimeObject($settlement); - $settlementEoM = self::isLastDayOfMonth($settlementDate); + $settlementEoM = Helpers::isLastDayOfMonth($settlementDate); if ($settlementEoM) { ++$settlement; } @@ -340,26 +344,12 @@ class Coupons return self::couponFirstPeriodDate($settlement, $maturity, $frequency, self::PERIOD_DATE_PREVIOUS); } - /** - * isLastDayOfMonth. - * - * Returns a boolean TRUE/FALSE indicating if this date is the last date of the month - * - * @param DateTime $testDate The date for testing - * - * @return bool - */ - private static function isLastDayOfMonth(DateTime $testDate) - { - return $testDate->format('d') === $testDate->format('t'); - } - private static function couponFirstPeriodDate($settlement, $maturity, int $frequency, $next) { $months = 12 / $frequency; $result = Date::excelToDateTimeObject($maturity); - $maturityEoM = self::isLastDayOfMonth($result); + $maturityEoM = Helpers::isLastDayOfMonth($result); while ($settlement < Date::PHPToExcel($result)) { $result->modify('-' . $months . ' months'); @@ -375,62 +365,10 @@ class Coupons return Date::PHPToExcel($result); } - private static function validateInputDate($date) - { - $date = DateTimeExcel\Helpers::getDateValue($date); - if (is_string($date)) { - throw new Exception(Functions::VALUE()); - } - - return $date; - } - - private static function validateSettlementDate($settlement) - { - return self::validateInputDate($settlement); - } - - private static function validateMaturityDate($maturity) - { - return self::validateInputDate($maturity); - } - private static function validateCouponPeriod($settlement, $maturity): void { if ($settlement >= $maturity) { throw new Exception(Functions::NAN()); } } - - private static function validateFrequency($frequency): int - { - if (!is_numeric($frequency)) { - throw new Exception(Functions::NAN()); - } - - $frequency = (int) $frequency; - if ( - ($frequency !== self::FREQUENCY_ANNUAL) && - ($frequency !== self::FREQUENCY_SEMI_ANNUAL) && - ($frequency !== self::FREQUENCY_QUARTERLY) - ) { - throw new Exception(Functions::NAN()); - } - - return $frequency; - } - - private static function validateBasis($basis): int - { - if (!is_numeric($basis)) { - throw new Exception(Functions::NAN()); - } - - $basis = (int) $basis; - if (($basis < 0) || ($basis > 4)) { - throw new Exception(Functions::NAN()); - } - - return $basis; - } } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php index 8770242f..89dc226c 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php @@ -7,6 +7,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; class Depreciation { + use BaseValidations; + /** * DB. * @@ -203,11 +205,7 @@ class Depreciation private static function validateCost($cost, bool $negativeValueAllowed = false): float { - if (!is_numeric($cost)) { - throw new Exception(Functions::VALUE()); - } - - $cost = (float) $cost; + $cost = self::validateFloat($cost); if ($cost < 0.0 && $negativeValueAllowed === false) { throw new Exception(Functions::NAN()); } @@ -217,11 +215,7 @@ class Depreciation private static function validateSalvage($salvage, bool $negativeValueAllowed = false): float { - if (!is_numeric($salvage)) { - throw new Exception(Functions::VALUE()); - } - - $salvage = (float) $salvage; + $salvage = self::validateFloat($salvage); if ($salvage < 0.0 && $negativeValueAllowed === false) { throw new Exception(Functions::NAN()); } @@ -231,11 +225,7 @@ class Depreciation private static function validateLife($life, bool $negativeValueAllowed = false): float { - if (!is_numeric($life)) { - throw new Exception(Functions::VALUE()); - } - - $life = (float) $life; + $life = self::validateFloat($life); if ($life < 0.0 && $negativeValueAllowed === false) { throw new Exception(Functions::NAN()); } @@ -245,11 +235,7 @@ class Depreciation private static function validatePeriod($period, bool $negativeValueAllowed = false): float { - if (!is_numeric($period)) { - throw new Exception(Functions::VALUE()); - } - - $period = (float) $period; + $period = self::validateFloat($period); if ($period <= 0.0 && $negativeValueAllowed === false) { throw new Exception(Functions::NAN()); } @@ -259,11 +245,7 @@ class Depreciation private static function validateMonth($month): int { - if (!is_numeric($month)) { - throw new Exception(Functions::VALUE()); - } - - $month = (int) $month; + $month = self::validateInt($month); if ($month < 1) { throw new Exception(Functions::NAN()); } @@ -273,11 +255,7 @@ class Depreciation private static function validateFactor($factor): float { - if (!is_numeric($factor)) { - throw new Exception(Functions::VALUE()); - } - - $factor = (float) $factor; + $factor = self::validateFloat($factor); if ($factor <= 0.0) { throw new Exception(Functions::NAN()); } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Helpers.php b/src/PhpSpreadsheet/Calculation/Financial/Helpers.php index 08c942ab..79ef61e3 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Helpers.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; +use DateTimeInterface; use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -47,4 +48,18 @@ class Helpers return Functions::NAN(); } + + /** + * isLastDayOfMonth. + * + * Returns a boolean TRUE/FALSE indicating if this date is the last date of the month + * + * @param DateTimeInterface $date The date for testing + * + * @return bool + */ + public static function isLastDayOfMonth(DateTimeInterface $date) + { + return $date->format('d') === $date->format('t'); + } } diff --git a/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php b/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php index 04b43e32..ed0fec75 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php +++ b/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php @@ -2,10 +2,13 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; class InterestRate { + use BaseValidations; + /** * EFFECT. * @@ -25,16 +28,17 @@ class InterestRate $nominalRate = Functions::flattenSingleValue($nominalRate); $periodsPerYear = Functions::flattenSingleValue($periodsPerYear); - // Validate parameters - if (!is_numeric($nominalRate) || !is_numeric($periodsPerYear)) { - return Functions::VALUE(); + try { + $nominalRate = self::validateFloat($nominalRate); + $periodsPerYear = self::validateInt($periodsPerYear); + } catch (Exception $e) { + return $e->getMessage(); } + if ($nominalRate <= 0 || $periodsPerYear < 1) { return Functions::NAN(); } - $periodsPerYear = (int) $periodsPerYear; - return ((1 + $nominalRate / $periodsPerYear) ** $periodsPerYear) - 1; } @@ -53,16 +57,17 @@ class InterestRate $effectiveRate = Functions::flattenSingleValue($effectiveRate); $periodsPerYear = Functions::flattenSingleValue($periodsPerYear); - // Validate parameters - if (!is_numeric($effectiveRate) || !is_numeric($periodsPerYear)) { - return Functions::VALUE(); + try { + $effectiveRate = self::validateFloat($effectiveRate); + $periodsPerYear = self::validateInt($periodsPerYear); + } catch (Exception $e) { + return $e->getMessage(); } + if ($effectiveRate <= 0 || $periodsPerYear < 1) { return Functions::NAN(); } - $periodsPerYear = (int) $periodsPerYear; - // Calculate return $periodsPerYear * (($effectiveRate + 1) ** (1 / $periodsPerYear) - 1); } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php new file mode 100644 index 00000000..f81ea13c --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php @@ -0,0 +1,143 @@ +getMessage(); + } + + $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return $daysBetweenIssueAndSettlement; + } + $daysBetweenFirstInterestAndSettlement = DateTime::YEARFRAC($firstinterest, $settlement, $basis); + if (!is_numeric($daysBetweenFirstInterestAndSettlement)) { + // return date error + return $daysBetweenFirstInterestAndSettlement; + } + + return $parValue * $rate * $daysBetweenIssueAndSettlement; + } + + /** + * ACCRINTM. + * + * Returns the accrued interest for a security that pays interest at maturity. + * + * Excel Function: + * ACCRINTM(issue,settlement,rate[,par[,basis]]) + * + * @param mixed $issue The security's issue date + * @param mixed $settlement The security's settlement (or maturity) date + * @param mixed (float) $rate The security's annual coupon rate + * @param mixed (float) $par The security's par value. + * If you omit par, ACCRINT uses $1,000. + * @param mixed (int) $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 + * @param mixed $parValue + * + * @return float|string Result, or a string containing an error + */ + public static function atMaturity($issue, $settlement, $rate, $parValue = 1000, $basis = 0) + { + $issue = Functions::flattenSingleValue($issue); + $settlement = Functions::flattenSingleValue($settlement); + $rate = Functions::flattenSingleValue($rate); + $parValue = ($parValue === null) ? 1000 : Functions::flattenSingleValue($parValue); + $basis = ($basis === null) ? 0 : Functions::flattenSingleValue($basis); + + try { + $issue = self::validateIssueDate($issue); + $settlement = self::validateSettlementDate($settlement); + self::validateSecurityPeriod($issue, $settlement); + $rate = self::validateRate($rate); + $parValue = self::validateParValue($parValue); + $basis = self::validateBasis($basis); + } catch (Exception $e) { + return $e->getMessage(); + } + + $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + if (!is_numeric($daysBetweenIssueAndSettlement)) { + // return date error + return $daysBetweenIssueAndSettlement; + } + + return $parValue * $rate * $daysBetweenIssueAndSettlement; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php index 2a5e5dd2..bd197d7f 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/BaseValidations.php @@ -7,31 +7,35 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities\Constants as SecuritiesConstants; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -abstract class BaseValidations +trait BaseValidations { - protected static function validateInputDate($date) + protected static function validateDate($date) { - $date = DateTimeExcel\Helpers::getDateValue($date); - if (is_string($date)) { + return DateTimeExcel\Helpers::getDateValue($date); + } + + protected static function validateFloat($value): float + { + if (!is_numeric($value)) { throw new Exception(Functions::VALUE()); } - return $date; + return (float) $value; } protected static function validateSettlementDate($settlement) { - return self::validateInputDate($settlement); + return self::validateDate($settlement); } protected static function validateMaturityDate($maturity) { - return self::validateInputDate($maturity); + return self::validateDate($maturity); } protected static function validateIssueDate($issue) { - return self::validateInputDate($issue); + return self::validateDate($issue); } protected static function validateSecurityPeriod($settlement, $maturity): void @@ -43,11 +47,7 @@ abstract class BaseValidations protected static function validateRate($rate): float { - if (!is_numeric($rate)) { - throw new Exception(Functions::VALUE()); - } - - $rate = (float) $rate; + $rate = self::validateFloat($rate); if ($rate < 0.0) { throw new Exception(Functions::NAN()); } @@ -55,13 +55,19 @@ abstract class BaseValidations return $rate; } - protected static function validatePrice($price): float + protected static function validateParValue($parValue): float { - if (!is_numeric($price)) { - throw new Exception(Functions::VALUE()); + $parValue = self::validateFloat($parValue); + if ($parValue < 0.0) { + throw new Exception(Functions::NAN()); } - $price = (float) $price; + return $parValue; + } + + protected static function validatePrice($price): float + { + $price = self::validateFloat($price); if ($price < 0.0) { throw new Exception(Functions::NAN()); } @@ -71,11 +77,7 @@ abstract class BaseValidations protected static function validateYield($yield): float { - if (!is_numeric($yield)) { - throw new Exception(Functions::VALUE()); - } - - $yield = (float) $yield; + $yield = self::validateFloat($yield); if ($yield < 0.0) { throw new Exception(Functions::NAN()); } @@ -85,11 +87,7 @@ abstract class BaseValidations protected static function validateRedemption($redemption): float { - if (!is_numeric($redemption)) { - throw new Exception(Functions::VALUE()); - } - - $redemption = (float) $redemption; + $redemption = self::validateFloat($redemption); if ($redemption <= 0.0) { throw new Exception(Functions::NAN()); } @@ -99,11 +97,7 @@ abstract class BaseValidations protected static function validateDiscount($discount): float { - if (!is_numeric($discount)) { - throw new Exception(Functions::VALUE()); - } - - $discount = (float) $discount; + $discount = self::validateFloat($discount); if ($discount <= 0.0) { throw new Exception(Functions::NAN()); } diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php index 18a0a2e1..6b04d6d9 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php @@ -8,8 +8,10 @@ use PhpOffice\PhpSpreadsheet\Calculation\Financial\Coupons; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -class Price extends BaseValidations +class Price { + use BaseValidations; + /** * PRICE. * diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php index 86151904..46c3bb05 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Yields.php @@ -7,8 +7,10 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Financial\Helpers; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -class Yields extends BaseValidations +class Yields { + use BaseValidations; + /** * YIELDDISC. * diff --git a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php index 966500bf..8fd47ba6 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php +++ b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php @@ -8,6 +8,8 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; class TreasuryBill { + use BaseValidations; + /** * TBILLEQ. * @@ -29,8 +31,8 @@ class TreasuryBill $discount = Functions::flattenSingleValue($discount); try { - $maturity = DateTimeExcel\Helpers::getDateValue($maturity); - $settlement = DateTimeExcel\Helpers::getDateValue($settlement); + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); } catch (Exception $e) { return $e->getMessage(); } @@ -75,8 +77,8 @@ class TreasuryBill $discount = Functions::flattenSingleValue($discount); try { - $maturity = DateTimeExcel\Helpers::getDateValue($maturity); - $settlement = DateTimeExcel\Helpers::getDateValue($settlement); + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); } catch (Exception $e) { return $e->getMessage(); } @@ -126,8 +128,8 @@ class TreasuryBill $price = Functions::flattenSingleValue($price); try { - $maturity = DateTimeExcel\Helpers::getDateValue($maturity); - $settlement = DateTimeExcel\Helpers::getDateValue($settlement); + $settlement = self::validateSettlementDate($settlement); + $maturity = self::validateMaturityDate($maturity); } catch (Exception $e) { return $e->getMessage(); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintMTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintMTest.php index 597db5c2..908e4862 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintMTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintMTest.php @@ -21,7 +21,7 @@ class AccrintMTest extends TestCase public function testACCRINTM($expectedResult, ...$args): void { $result = Financial::ACCRINTM(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } public function providerACCRINTM() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintTest.php index edb79230..2d31c4cc 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/AccrintTest.php @@ -21,7 +21,7 @@ class AccrintTest extends TestCase public function testACCRINT($expectedResult, ...$args): void { $result = Financial::ACCRINT(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-8); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } public function providerACCRINT() diff --git a/tests/data/Calculation/Financial/ACCRINT.php b/tests/data/Calculation/Financial/ACCRINT.php index 58f6b636..1852e7e3 100644 --- a/tests/data/Calculation/Financial/ACCRINT.php +++ b/tests/data/Calculation/Financial/ACCRINT.php @@ -4,72 +4,83 @@ return [ [ - 16.666666666666998, - '2008-03-01', - '2008-08-31', - '2008-05-01', - 0.10000000000000001, - 1000, - 2, - 0, + 16.6666666666666, + '2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0, ], [ - 15.555555555555999, - '2008-03-05', - '2008-08-31', - '2008-05-01', - 0.10000000000000001, - 1000, - 2, - 0, + 15.5555555555559, + '2008-03-05', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0, + ], + [ + 15.5555555555559, + '2008-03-05', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0, true, + ], + [ + 7.22222222222222, + '2008-04-05', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0, true, ], [ 200, - '2010-01-01', - '2010-06-30', - '2010-04-01', - 0.080000000000000002, - 10000, - 4, + '2010-01-01', '2010-06-30', '2010-04-01', 0.08, 10000, 4, + ], + [ + 1600, + '2012-01-01', '2012-04-01', '2013-12-31', 0.08, 10000, 4, + ], + [ + 32.363013698630134, + '2012-01-01', '2012-03-31', '2012-02-15', 0.0525, 5000, 4, 3, 1, + ], + [ + 6.472602739726027, + '2012-01-01', '2012-03-31', '2012-02-15', 0.0525, 1000, 4, 3, 1, + ], + [ + 18.05555555555555, + '2017-08-05', '2017-11-10', '2017-10-10', 0.05, 2000, 4, 0, 1, ], [ '#NUM!', - '2008-03-05', - '2008-08-31', - '2008-05-01', - -0.10000000000000001, - 1000, - 2, - 0, + '2008-03-05', '2008-08-31', '2008-05-01', -0.10, 1000, 2, 0, ], [ '#VALUE!', - 'Invalid Date', - '2008-08-31', - '2008-05-01', - 0.10000000000000001, - 1000, - 2, - 0, + 'Invalid Date', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 0, ], [ '#VALUE!', - '2008-03-01', - '2008-08-31', - '2008-05-01', - 'ABC', - 1000, - 2, - 0, + '2008-03-01', '2008-08-31', '2008-05-01', 'ABC', 1000, 2, 0, ], - [ + 'Non-numeric Rate' => [ '#VALUE!', - '2008-03-01', - '2008-08-31', - '2008-05-01', - 0.10000000000000001, - 1000, - 2, - 'ABC', + '2008-03-01', '2008-08-31', '2008-05-01', 'NaN', 1000, 2, 0, + ], + 'Invalid Rate' => [ + '#NUM!', + '2008-03-01', '2008-08-31', '2008-05-01', -0.10, 1000, 2, 0, + ], + 'Non-numeric Par Value' => [ + '#VALUE!', + '2008-03-01', '2008-08-31', '2008-05-01', 0.10, 'NaN', 2, 0, + ], + 'Invalid Par Value' => [ + '#NUM!', + '2008-03-01', '2008-08-31', '2008-05-01', 0.10, -1000, 2, 0, + ], + 'Non-numeric Frequency' => [ + '#VALUE!', + '2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 'NaN', 0, + ], + 'Invalid Frequency' => [ + '#NUM!', + '2008-03-01', '2008-08-31', '2008-05-01', 0.10, -1000, 3, 0, + ], + 'Non-numeric Basis' => [ + '#VALUE!', + '2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 2, 'ABC', + ], + 'Invalid Basis' => [ + '#NUM!', + '2008-03-01', '2008-08-31', '2008-05-01', 0.10, 1000, 2, -2, ], ]; diff --git a/tests/data/Calculation/Financial/ACCRINTM.php b/tests/data/Calculation/Financial/ACCRINTM.php index 7949b1ad..e442b6f8 100644 --- a/tests/data/Calculation/Financial/ACCRINTM.php +++ b/tests/data/Calculation/Financial/ACCRINTM.php @@ -5,41 +5,50 @@ return [ [ 20.547945205478999, - '2008-04-01', - '2008-06-15', - 0.10000000000000001, - 1000, - 3, + '2008-04-01', '2008-06-15', 0.10, 1000, 3, ], [ 800, - '2010-01-01', - '2010-12-31', - 0.080000000000000002, - 10000, + '2010-01-01', '2010-12-31', 0.08, 10000, + ], + [ + 365.958904109589, + '2012-01-01', '2013-02-15', 0.065, 5000, 3, + ], + [ + 73.1917808219178, + '2012-01-01', '2013-02-15', 0.065, 1000, 3, ], [ '#NUM!', - '2008-03-05', - '2008-08-31', - -0.10000000000000001, - 1000, - 2, + '2008-03-05', '2008-08-31', -0.10, 1000, 2, ], [ '#VALUE!', - 'Invalid Date', - '2008-08-31', - 0.10000000000000001, - 1000, - 2, + 'Invalid Date', '2008-08-31', 0.10, 1000, 2, ], - [ + 'Non-numeric Rate' => [ '#VALUE!', - '2008-03-01', - '2008-08-31', - 'ABC', - 1000, - 2, + '2008-03-01', '2008-08-31', 'NaN', 1000, 2, + ], + 'Invalid Rate' => [ + '#NUM!', + '2008-03-01', '2008-08-31', -0.10, 1000, 2, + ], + 'Non-numeric Par Value' => [ + '#VALUE!', + '2008-03-01', '2008-08-31', 0.10, 'NaN', 2, + ], + 'Invalid Par Value' => [ + '#NUM!', + '2008-03-01', '2008-08-31', 0.10, -1000, 2, + ], + 'Non-numeric Basis' => [ + '#VALUE!', + '2008-03-01', '2008-08-31', 0.10, 1000, 'NaN', + ], + 'Invalid Basis' => [ + '#NUM!', + '2008-03-01', '2008-08-31', 0.10, 1000, 99, ], ]; diff --git a/tests/data/Calculation/Financial/AMORDEGRC.php b/tests/data/Calculation/Financial/AMORDEGRC.php index f4007033..ef3ef1e0 100644 --- a/tests/data/Calculation/Financial/AMORDEGRC.php +++ b/tests/data/Calculation/Financial/AMORDEGRC.php @@ -47,6 +47,14 @@ return [ 42, 150, '2011-01-01', '2011-09-30', 20, 1, 0.4, 4, ], + [ + 2813, + 10000, '2012-03-01', '2012-12-31', 1500, 1, 0.3, 1, + ], + [ + '#VALUE!', + 'NaN', '2012-03-01', '2020-12-25', 20, 1, 0.2, 4, + ], [ '#VALUE!', 550, 'notADate', '2020-12-25', 20, 1, 0.2, 4, @@ -55,4 +63,8 @@ return [ '#VALUE!', 550, '2011-01-01', 'notADate', 20, 1, 0.2, 4, ], + [ + '#VALUE!', + 550, '2012-03-01', '2020-12-25', 'NaN', 1, 0.2, 4, + ], ]; diff --git a/tests/data/Calculation/Financial/COUPDAYBS.php b/tests/data/Calculation/Financial/COUPDAYBS.php index c2208f7d..7a805fdf 100644 --- a/tests/data/Calculation/Financial/COUPDAYBS.php +++ b/tests/data/Calculation/Financial/COUPDAYBS.php @@ -45,7 +45,7 @@ return [ 1, ], 'Non-Numeric Frequency' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 'NaN', @@ -59,7 +59,7 @@ return [ -1, ], 'Non-Numeric Basis' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 4, diff --git a/tests/data/Calculation/Financial/COUPDAYS.php b/tests/data/Calculation/Financial/COUPDAYS.php index acec49d9..2cd2469c 100644 --- a/tests/data/Calculation/Financial/COUPDAYS.php +++ b/tests/data/Calculation/Financial/COUPDAYS.php @@ -59,7 +59,7 @@ return [ 1, ], 'Non-Numeric Frequency' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 'NaN', @@ -73,7 +73,7 @@ return [ -1, ], 'Non-Numeric Basis' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 4, diff --git a/tests/data/Calculation/Financial/COUPDAYSNC.php b/tests/data/Calculation/Financial/COUPDAYSNC.php index 87951dd1..6a7c5bb5 100644 --- a/tests/data/Calculation/Financial/COUPDAYSNC.php +++ b/tests/data/Calculation/Financial/COUPDAYSNC.php @@ -38,7 +38,7 @@ return [ 1, ], 'Non-Numeric Frequency' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 'NaN', @@ -52,7 +52,7 @@ return [ -1, ], 'Non-Numeric Basis' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 4, diff --git a/tests/data/Calculation/Financial/COUPNCD.php b/tests/data/Calculation/Financial/COUPNCD.php index e3d452e5..222c6aa5 100644 --- a/tests/data/Calculation/Financial/COUPNCD.php +++ b/tests/data/Calculation/Financial/COUPNCD.php @@ -38,7 +38,7 @@ return [ 1, ], 'Non-Numeric Frequency' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 'NaN', @@ -52,7 +52,7 @@ return [ -1, ], 'Non-Numeric Basis' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 4, diff --git a/tests/data/Calculation/Financial/COUPNUM.php b/tests/data/Calculation/Financial/COUPNUM.php index b9ad73fa..5af5fd7b 100644 --- a/tests/data/Calculation/Financial/COUPNUM.php +++ b/tests/data/Calculation/Financial/COUPNUM.php @@ -39,7 +39,7 @@ return [ 1, ], 'Non-Numeric Frequency' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 'NaN', @@ -53,7 +53,7 @@ return [ -1, ], 'Non-Numeric Basis' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 4, diff --git a/tests/data/Calculation/Financial/COUPPCD.php b/tests/data/Calculation/Financial/COUPPCD.php index 6d2e2f22..07c00d19 100644 --- a/tests/data/Calculation/Financial/COUPPCD.php +++ b/tests/data/Calculation/Financial/COUPPCD.php @@ -38,7 +38,7 @@ return [ 1, ], 'Non-Numeric Frequency' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 'NaN', @@ -52,7 +52,7 @@ return [ -1, ], 'Non-Numeric Basis' => [ - '#NUM!', + '#VALUE!', '25-Jan-2007', '15-Nov-2008', 4, From ec2531411da6ea57c5d1aa5ec2c7179ee6f58bc4 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 27 Mar 2021 13:29:58 +0100 Subject: [PATCH 66/89] Start implementing Newton-Raphson for the inverse of Statistical Distributions (#1958) * Start implementing Newton-Raphson for the inverse of Statistical Distributions, starting with the two-tailed Student-T * Additional unit tests and validations * Use the new Newton Raphson class for calculating the Inverse of ChiSquared * Extract Weibull distribution, and provide unit tests --- .../Calculation/Calculation.php | 10 +- .../Calculation/Statistical.php | 129 +++--------------- .../Statistical/Distributions/ChiSquared.php | 52 +------ .../Statistical/Distributions/GammaBase.php | 7 +- .../Distributions/NewtonRaphson.php | 62 +++++++++ .../Statistical/Distributions/StudentT.php | 127 +++++++++++++++++ .../Statistical/Distributions/Weibull.php | 51 +++++++ .../Functions/Statistical/TDistTest.php | 28 ++++ .../Functions/Statistical/TinvTest.php | 27 ++++ .../Functions/Statistical/WeibullTest.php | 29 ++++ tests/data/Calculation/Statistical/TDIST.php | 56 ++++++++ tests/data/Calculation/Statistical/TINV.php | 32 +++++ .../data/Calculation/Statistical/WEIBULL.php | 48 +++++++ 13 files changed, 493 insertions(+), 165 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TDistTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TinvTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/WeibullTest.php create mode 100644 tests/data/Calculation/Statistical/TDIST.php create mode 100644 tests/data/Calculation/Statistical/TINV.php create mode 100644 tests/data/Calculation/Statistical/WEIBULL.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index e1ccb74b..6e98fcb0 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -2391,7 +2391,7 @@ class Calculation ], 'TDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'TDIST'], + 'functionCall' => [Statistical\Distributions\StudentT::class, 'distribution'], 'argumentCount' => '3', ], 'T.DIST' => [ @@ -2431,12 +2431,12 @@ class Calculation ], 'TINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'TINV'], + 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'], 'argumentCount' => '2', ], 'T.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'TINV'], + 'functionCall' => [Statistical\Distributions\StudentT::class, 'inverse'], 'argumentCount' => '2', ], 'T.INV.2T' => [ @@ -2581,12 +2581,12 @@ class Calculation ], 'WEIBULL' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'WEIBULL'], + 'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'], 'argumentCount' => '4', ], 'WEIBULL.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'WEIBULL'], + 'functionCall' => [Statistical\Distributions\Weibull::class, 'distribution'], 'argumentCount' => '4', ], 'WORKDAY' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 695d7cbd..5f557431 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -2140,6 +2140,11 @@ class Statistical * * Returns the probability of Student's T distribution. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\StudentT::distribution() + * Use the distribution() method in the Statistical\Distributions\StudentT class instead + * * @param float $value Value for the function * @param float $degrees degrees of freedom * @param float $tails number of tails (1 or 2) @@ -2148,55 +2153,7 @@ class Statistical */ public static function TDIST($value, $degrees, $tails) { - $value = Functions::flattenSingleValue($value); - $degrees = floor(Functions::flattenSingleValue($degrees)); - $tails = floor(Functions::flattenSingleValue($tails)); - - if ((is_numeric($value)) && (is_numeric($degrees)) && (is_numeric($tails))) { - if (($value < 0) || ($degrees < 1) || ($tails < 1) || ($tails > 2)) { - return Functions::NAN(); - } - // tdist, which finds the probability that corresponds to a given value - // of t with k degrees of freedom. This algorithm is translated from a - // pascal function on p81 of "Statistical Computing in Pascal" by D - // Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd: - // London). The above Pascal algorithm is itself a translation of the - // fortran algoritm "AS 3" by B E Cooper of the Atlas Computer - // Laboratory as reported in (among other places) "Applied Statistics - // Algorithms", editied by P Griffiths and I D Hill (1985; Ellis - // Horwood Ltd.; W. Sussex, England). - $tterm = $degrees; - $ttheta = atan2($value, sqrt($tterm)); - $tc = cos($ttheta); - $ts = sin($ttheta); - - if (($degrees % 2) == 1) { - $ti = 3; - $tterm = $tc; - } else { - $ti = 2; - $tterm = 1; - } - - $tsum = $tterm; - while ($ti < $degrees) { - $tterm *= $tc * $tc * ($ti - 1) / $ti; - $tsum += $tterm; - $ti += 2; - } - $tsum *= $ts; - if (($degrees % 2) == 1) { - $tsum = Functions::M_2DIVPI * ($tsum + $ttheta); - } - $tValue = 0.5 * (1 + $tsum); - if ($tails == 1) { - return 1 - abs($tValue); - } - - return 1 - abs((1 - $tValue) - $tValue); - } - - return Functions::VALUE(); + return Statistical\Distributions\StudentT::distribution($value, $degrees, $tails); } /** @@ -2204,6 +2161,11 @@ class Statistical * * Returns the one-tailed probability of the chi-squared distribution. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\StudentT::inverse() + * Use the inverse() method in the Statistical\Distributions\StudentT class instead + * * @param float $probability Probability for the function * @param float $degrees degrees of freedom * @@ -2211,50 +2173,7 @@ class Statistical */ public static function TINV($probability, $degrees) { - $probability = Functions::flattenSingleValue($probability); - $degrees = floor(Functions::flattenSingleValue($degrees)); - - if ((is_numeric($probability)) && (is_numeric($degrees))) { - $xLo = 100; - $xHi = 0; - - $x = $xNew = 1; - $dx = 1; - $i = 0; - - while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { - // Apply Newton-Raphson step - $result = self::TDIST($x, $degrees, 2); - $error = $result - $probability; - if ($error == 0.0) { - $dx = 0; - } elseif ($error < 0.0) { - $xLo = $x; - } else { - $xHi = $x; - } - // Avoid division by zero - if ($result != 0.0) { - $dx = $error / $result; - $xNew = $x - $dx; - } - // If the NR fails to converge (which for example may be the - // case if the initial guess is too rough) we apply a bisection - // step to determine a more narrow interval around the root. - if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) { - $xNew = ($xLo + $xHi) / 2; - $dx = $xNew - $x; - } - $x = $xNew; - } - if ($i == self::MAX_ITERATIONS) { - return Functions::NA(); - } - - return round($x, 12); - } - - return Functions::VALUE(); + return Statistical\Distributions\StudentT::inverse($probability, $degrees); } /** @@ -2421,6 +2340,11 @@ class Statistical * Returns the Weibull distribution. Use this distribution in reliability * analysis, such as calculating a device's mean time to failure. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Weibull::distribution() + * Use the distribution() method in the Statistical\Distributions\Weibull class instead + * * @param float $value * @param float $alpha Alpha Parameter * @param float $beta Beta Parameter @@ -2430,24 +2354,7 @@ class Statistical */ public static function WEIBULL($value, $alpha, $beta, $cumulative) { - $value = Functions::flattenSingleValue($value); - $alpha = Functions::flattenSingleValue($alpha); - $beta = Functions::flattenSingleValue($beta); - - if ((is_numeric($value)) && (is_numeric($alpha)) && (is_numeric($beta))) { - if (($value < 0) || ($alpha <= 0) || ($beta <= 0)) { - return Functions::NAN(); - } - if ((is_numeric($cumulative)) || (is_bool($cumulative))) { - if ($cumulative) { - return 1 - exp(0 - ($value / $beta) ** $alpha); - } - - return ($alpha / $beta ** $alpha) * $value ** ($alpha - 1) * exp(0 - ($value / $beta) ** $alpha); - } - } - - return Functions::VALUE(); + return Statistical\Distributions\Weibull::distribution($value, $alpha, $beta, $cumulative); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 2d5e4496..636189b5 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -73,55 +73,13 @@ class ChiSquared return Functions::NAN(); } - return self::calculateInverse($degrees, $probability); - } - - /** - * @return float|string - */ - protected static function calculateInverse(int $degrees, float $probability) - { - $xLo = 100; - $xHi = 0; - - $x = $xNew = 1; - $dx = 1; - $i = 0; - - while ((abs($dx) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { - // Apply Newton-Raphson step - $result = 1 - (Gamma::incompleteGamma($degrees / 2, $x / 2) + $callback = function ($value) use ($degrees) { + return 1 - (Gamma::incompleteGamma($degrees / 2, $value / 2) / Gamma::gammaValue($degrees / 2)); - $error = $result - $probability; + }; - if ($error == 0.0) { - $dx = 0; - } elseif ($error < 0.0) { - $xLo = $x; - } else { - $xHi = $x; - } + $newtonRaphson = new NewtonRaphson($callback); - // Avoid division by zero - if ($result != 0.0) { - $dx = $error / $result; - $xNew = $x - $dx; - } - - // If the NR fails to converge (which for example may be the - // case if the initial guess is too rough) we apply a bisection - // step to determine a more narrow interval around the root. - if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) { - $xNew = ($xLo + $xHi) / 2; - $dx = $xNew - $x; - } - $x = $xNew; - } - - if ($i === self::MAX_ITERATIONS) { - return Functions::NA(); - } - - return $x; + return $newtonRaphson->execute($probability); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php index ae951af3..3f76787d 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php @@ -36,8 +36,11 @@ abstract class GammaBase while ((abs($dx) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { // Apply Newton-Raphson step - $error = self::calculateDistribution($x, $alpha, $beta, true) - $probability; - if ($error < 0.0) { + $result = self::calculateDistribution($x, $alpha, $beta, true); + $error = $result - $probability; + if ($error == 0.0) { + $dx = 0; + } elseif ($error < 0.0) { $xLo = $x; } else { $xHi = $x; diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php new file mode 100644 index 00000000..298cdfaf --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php @@ -0,0 +1,62 @@ +callback = $callback; + } + + public function execute($probability) + { + $xLo = 100; + $xHi = 0; + + $x = $xNew = 1; + $dx = 1; + $i = 0; + + while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { + // Apply Newton-Raphson step + $result = call_user_func($this->callback, $x); + $error = $result - $probability; + + if ($error == 0.0) { + $dx = 0; + } elseif ($error < 0.0) { + $xLo = $x; + } else { + $xHi = $x; + } + + // Avoid division by zero + if ($result != 0.0) { + $dx = $error / $result; + $xNew = $x - $dx; + } + + // If the NR fails to converge (which for example may be the + // case if the initial guess is too rough) we apply a bisection + // step to determine a more narrow interval around the root. + if (($xNew < $xLo) || ($xNew > $xHi) || ($result == 0.0)) { + $xNew = ($xLo + $xHi) / 2; + $dx = $xNew - $x; + } + $x = $xNew; + } + + if ($i == self::MAX_ITERATIONS) { + return Functions::NA(); + } + + return $x; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php new file mode 100644 index 00000000..a6d23c6b --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php @@ -0,0 +1,127 @@ +getMessage(); + } + + if (($value < 0) || ($degrees < 1) || ($tails < 1) || ($tails > 2)) { + return Functions::NAN(); + } + + return self::calculateDistribution($value, $degrees, $tails); + } + + /** + * TINV. + * + * Returns the one-tailed probability of the chi-squared distribution. + * + * @param mixed (float) $probability Probability for the function + * @param mixed (float) $degrees degrees of freedom + * + * @return float|string The result, or a string containing an error + */ + public static function inverse($probability, $degrees) + { + $probability = Functions::flattenSingleValue($probability); + $degrees = Functions::flattenSingleValue($degrees); + + try { + $probability = self::validateFloat($probability); + $degrees = self::validateInt($degrees); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($probability < 0.0 || $probability > 1.0 || $degrees <= 0) { + return Functions::NAN(); + } + + $callback = function ($value) use ($degrees) { + return self::distribution($value, $degrees, 2); + }; + + $newtonRaphson = new NewtonRaphson($callback); + + return $newtonRaphson->execute($probability); + } + + /** + * @return float|int + */ + private static function calculateDistribution(float $value, int $degrees, int $tails) + { + // tdist, which finds the probability that corresponds to a given value + // of t with k degrees of freedom. This algorithm is translated from a + // pascal function on p81 of "Statistical Computing in Pascal" by D + // Cooke, A H Craven & G M Clark (1985: Edward Arnold (Pubs.) Ltd: + // London). The above Pascal algorithm is itself a translation of the + // fortran algoritm "AS 3" by B E Cooper of the Atlas Computer + // Laboratory as reported in (among other places) "Applied Statistics + // Algorithms", editied by P Griffiths and I D Hill (1985; Ellis + // Horwood Ltd.; W. Sussex, England). + $tterm = $degrees; + $ttheta = atan2($value, sqrt($tterm)); + $tc = cos($ttheta); + $ts = sin($ttheta); + + if (($degrees % 2) === 1) { + $ti = 3; + $tterm = $tc; + } else { + $ti = 2; + $tterm = 1; + } + + $tsum = $tterm; + while ($ti < $degrees) { + $tterm *= $tc * $tc * ($ti - 1) / $ti; + $tsum += $tterm; + $ti += 2; + } + + $tsum *= $ts; + if (($degrees % 2) == 1) { + $tsum = Functions::M_2DIVPI * ($tsum + $ttheta); + } + + $tValue = 0.5 * (1 + $tsum); + if ($tails == 1) { + return 1 - abs($tValue); + } + + return 1 - abs((1 - $tValue) - $tValue); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php new file mode 100644 index 00000000..2064c2e2 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php @@ -0,0 +1,51 @@ +getMessage(); + } + + if (($value < 0) || ($alpha <= 0) || ($beta <= 0)) { + return Functions::NAN(); + } + + if ($cumulative) { + return 1 - exp(0 - ($value / $beta) ** $alpha); + } + + return ($alpha / $beta ** $alpha) * $value ** ($alpha - 1) * exp(0 - ($value / $beta) ** $alpha); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TDistTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TDistTest.php new file mode 100644 index 00000000..a6a2c97e --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/TDistTest.php @@ -0,0 +1,28 @@ + Date: Sat, 27 Mar 2021 18:31:24 +0100 Subject: [PATCH 67/89] Difference in variance calculations between Excel/Gnumeric and Open/LibreOffice (#1959) * Difference in variance calculations between Excel/Gnumeric and Open/LibreOffice * Simplify STDEV() function logic by remembering that STDEV() is simply the square root of VAR(), so we can simply use the VAR() calculaion rather than duplicating the basic logic... and also allow for the differences between Excel/Gnumeric and Open/LibreOffice --- .../Statistical/StandardDeviations.php | 120 +++--------------- .../Calculation/Statistical/VarianceBase.php | 4 +- .../Calculation/Statistical/Variances.php | 2 + .../Functions/Statistical/StDevATest.php | 25 ++++ .../Functions/Statistical/StDevPATest.php | 25 ++++ .../Functions/Statistical/StDevPTest.php | 25 ++++ .../Functions/Statistical/StDevTest.php | 25 ++++ .../Functions/Statistical/VarATest.php | 25 ++++ .../Functions/Statistical/VarPATest.php | 25 ++++ .../Functions/Statistical/VarPTest.php | 25 ++++ .../Functions/Statistical/VarTest.php | 25 ++++ tests/data/Calculation/Statistical/AVEDEV.php | 4 + .../data/Calculation/Statistical/AVERAGE.php | 4 + .../data/Calculation/Statistical/AVERAGEA.php | 8 ++ tests/data/Calculation/Statistical/STDEV.php | 8 ++ tests/data/Calculation/Statistical/STDEVA.php | 8 ++ .../Calculation/Statistical/STDEVA_ODS.php | 20 +++ tests/data/Calculation/Statistical/STDEVP.php | 8 ++ .../data/Calculation/Statistical/STDEVPA.php | 8 ++ .../Calculation/Statistical/STDEVPA_ODS.php | 20 +++ .../Calculation/Statistical/STDEVP_ODS.php | 20 +++ .../Calculation/Statistical/STDEV_ODS.php | 20 +++ tests/data/Calculation/Statistical/VAR.php | 8 ++ tests/data/Calculation/Statistical/VARA.php | 8 ++ .../data/Calculation/Statistical/VARA_ODS.php | 16 +++ tests/data/Calculation/Statistical/VARP.php | 8 ++ tests/data/Calculation/Statistical/VARPA.php | 8 ++ .../Calculation/Statistical/VARPA_ODS.php | 16 +++ .../data/Calculation/Statistical/VARP_ODS.php | 16 +++ .../data/Calculation/Statistical/VAR_ODS.php | 16 +++ 30 files changed, 446 insertions(+), 104 deletions(-) create mode 100644 tests/data/Calculation/Statistical/STDEVA_ODS.php create mode 100644 tests/data/Calculation/Statistical/STDEVPA_ODS.php create mode 100644 tests/data/Calculation/Statistical/STDEVP_ODS.php create mode 100644 tests/data/Calculation/Statistical/STDEV_ODS.php create mode 100644 tests/data/Calculation/Statistical/VARA_ODS.php create mode 100644 tests/data/Calculation/Statistical/VARPA_ODS.php create mode 100644 tests/data/Calculation/Statistical/VARP_ODS.php create mode 100644 tests/data/Calculation/Statistical/VAR_ODS.php diff --git a/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php b/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php index 4f15615c..af271205 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/StandardDeviations.php @@ -2,9 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; - -class StandardDeviations extends VarianceBase +class StandardDeviations { /** * STDEV. @@ -21,34 +19,12 @@ class StandardDeviations extends VarianceBase */ public static function STDEV(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - $aMean = Averages::average($aArgs); - - if (!is_string($aMean)) { - $returnValue = 0.0; - $aCount = -1; - - foreach ($aArgs as $k => $arg) { - if ( - (is_bool($arg)) && - ((!Functions::isCellValue($k)) || (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) - ) { - $arg = (int) $arg; - } - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $returnValue += ($arg - $aMean) ** 2; - ++$aCount; - } - } - - if ($aCount > 0) { - return sqrt($returnValue / $aCount); - } + $result = Variances::VAR(...$args); + if (!is_numeric($result)) { + return $result; } - return Functions::DIV0(); + return sqrt((float) $result); } /** @@ -65,32 +41,12 @@ class StandardDeviations extends VarianceBase */ public static function STDEVA(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - $aMean = Averages::averageA($aArgs); - - if (!is_string($aMean)) { - $returnValue = 0.0; - $aCount = -1; - - foreach ($aArgs as $k => $arg) { - if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { - } else { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { - $arg = self::datatypeAdjustmentAllowStrings($arg); - $returnValue += ($arg - $aMean) ** 2; - ++$aCount; - } - } - } - - if ($aCount > 0) { - return sqrt($returnValue / $aCount); - } + $result = Variances::VARA(...$args); + if (!is_numeric($result)) { + return $result; } - return Functions::DIV0(); + return sqrt((float) $result); } /** @@ -107,34 +63,12 @@ class StandardDeviations extends VarianceBase */ public static function STDEVP(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - $aMean = Averages::average($aArgs); - - if (!is_string($aMean)) { - $returnValue = 0.0; - $aCount = 0; - - foreach ($aArgs as $k => $arg) { - if ( - (is_bool($arg)) && - ((!Functions::isCellValue($k)) || (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) - ) { - $arg = (int) $arg; - } - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $returnValue += ($arg - $aMean) ** 2; - ++$aCount; - } - } - - if ($aCount > 0) { - return sqrt($returnValue / $aCount); - } + $result = Variances::VARP(...$args); + if (!is_numeric($result)) { + return $result; } - return Functions::DIV0(); + return sqrt((float) $result); } /** @@ -151,31 +85,11 @@ class StandardDeviations extends VarianceBase */ public static function STDEVPA(...$args) { - $aArgs = Functions::flattenArrayIndexed($args); - - $aMean = Averages::averageA($aArgs); - - if (!is_string($aMean)) { - $returnValue = 0.0; - $aCount = 0; - - foreach ($aArgs as $k => $arg) { - if ((is_bool($arg)) && (!Functions::isMatrixValue($k))) { - } else { - // Is it a numeric value? - if ((is_numeric($arg)) || (is_bool($arg)) || ((is_string($arg) && ($arg != '')))) { - $arg = self::datatypeAdjustmentAllowStrings($arg); - $returnValue += ($arg - $aMean) ** 2; - ++$aCount; - } - } - } - - if ($aCount > 0) { - return sqrt($returnValue / $aCount); - } + $result = Variances::VARPA(...$args); + if (!is_numeric($result)) { + return $result; } - return Functions::DIV0(); + return sqrt((float) $result); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php b/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php index 9762ec84..e5334671 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/VarianceBase.php @@ -2,6 +2,8 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; + abstract class VarianceBase { protected static function datatypeAdjustmentAllowStrings($value) @@ -17,7 +19,7 @@ abstract class VarianceBase protected static function datatypeAdjustmentBooleans($value) { - if (is_bool($value)) { + if (is_bool($value) && (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_OPENOFFICE)) { return (int) $value; } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Variances.php b/src/PhpSpreadsheet/Calculation/Statistical/Variances.php index 78b08da9..ac9c3320 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Variances.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Variances.php @@ -29,6 +29,7 @@ class Variances extends VarianceBase $aCount = 0; foreach ($aArgs as $arg) { $arg = self::datatypeAdjustmentBooleans($arg); + // Is it a numeric value? if ((is_numeric($arg)) && (!is_string($arg))) { $summerA += ($arg * $arg); @@ -117,6 +118,7 @@ class Variances extends VarianceBase $aCount = 0; foreach ($aArgs as $arg) { $arg = self::datatypeAdjustmentBooleans($arg); + // Is it a numeric value? if ((is_numeric($arg)) && (!is_string($arg))) { $summerA += ($arg * $arg); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php index 9115db46..79e4482a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevATest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class StDevATest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerSTDEVA * @@ -23,4 +29,23 @@ class StDevATest extends TestCase { return require 'tests/data/Calculation/Statistical/STDEVA.php'; } + + /** + * @dataProvider providerOdsSTDEVA + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsSTDEVA($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::STDEVA($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsSTDEVA() + { + return require 'tests/data/Calculation/Statistical/STDEVA_ODS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPATest.php index 9d8921cc..b004e5b0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPATest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class StDevPATest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerSTDEVPA * @@ -23,4 +29,23 @@ class StDevPATest extends TestCase { return require 'tests/data/Calculation/Statistical/STDEVPA.php'; } + + /** + * @dataProvider providerOdsSTDEVPA + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsSTDEVPA($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::STDEVPA($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsSTDEVPA() + { + return require 'tests/data/Calculation/Statistical/STDEVPA_ODS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPTest.php index 47b058b3..7e45ec51 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevPTest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class StDevPTest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerSTDEVP * @@ -23,4 +29,23 @@ class StDevPTest extends TestCase { return require 'tests/data/Calculation/Statistical/STDEVP.php'; } + + /** + * @dataProvider providerOdsSTDEVP + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsSTDEVP($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::STDEVP($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsSTDEVP() + { + return require 'tests/data/Calculation/Statistical/STDEVP_ODS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevTest.php index 6a96fa2f..bc59869d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/StDevTest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class StDevTest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerSTDEV * @@ -23,4 +29,23 @@ class StDevTest extends TestCase { return require 'tests/data/Calculation/Statistical/STDEV.php'; } + + /** + * @dataProvider providerOdsSTDEV + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsSTDEV($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::STDEV($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsSTDEV() + { + return require 'tests/data/Calculation/Statistical/STDEV_ODS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarATest.php index 1b8b676f..8d664af4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarATest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class VarATest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerVARA * @@ -23,4 +29,23 @@ class VarATest extends TestCase { return require 'tests/data/Calculation/Statistical/VARA.php'; } + + /** + * @dataProvider providerOdsVARA + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsVARA($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::VARA($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsVARA() + { + return require 'tests/data/Calculation/Statistical/VARA_ODS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPATest.php index ee0cfee6..8240b5cf 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPATest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPATest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class VarPATest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerVARPA * @@ -23,4 +29,23 @@ class VarPATest extends TestCase { return require 'tests/data/Calculation/Statistical/VARPA.php'; } + + /** + * @dataProvider providerOdsVARPA + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsVARPA($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::VARPA($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsVARPA() + { + return require 'tests/data/Calculation/Statistical/VARPA_ODS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPTest.php index fe3af037..bbc5239c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarPTest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class VarPTest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerVARP * @@ -23,4 +29,23 @@ class VarPTest extends TestCase { return require 'tests/data/Calculation/Statistical/VARP.php'; } + + /** + * @dataProvider providerOdsVARP + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsVARP($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::VARP($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsVARP() + { + return require 'tests/data/Calculation/Statistical/VARP_ODS.php'; + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarTest.php index aa25f5cc..15aa98a0 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/VarTest.php @@ -2,11 +2,17 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; class VarTest extends TestCase { + protected function tearDown(): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); + } + /** * @dataProvider providerVAR * @@ -23,4 +29,23 @@ class VarTest extends TestCase { return require 'tests/data/Calculation/Statistical/VAR.php'; } + + /** + * @dataProvider providerOdsVAR + * + * @param mixed $expectedResult + * @param mixed $values + */ + public function testOdsVAR($expectedResult, $values): void + { + Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE); + + $result = Statistical::VARFunc($values); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + } + + public function providerOdsVAR() + { + return require 'tests/data/Calculation/Statistical/VAR_ODS.php'; + } } diff --git a/tests/data/Calculation/Statistical/AVEDEV.php b/tests/data/Calculation/Statistical/AVEDEV.php index 89713592..b2fa9a14 100644 --- a/tests/data/Calculation/Statistical/AVEDEV.php +++ b/tests/data/Calculation/Statistical/AVEDEV.php @@ -42,4 +42,8 @@ return [ '#VALUE!', [1, '2', 3.4, true, 5, null, 6.7, 'STRING', ''], ], + [ + '#NUM!', + [], + ], ]; diff --git a/tests/data/Calculation/Statistical/AVERAGE.php b/tests/data/Calculation/Statistical/AVERAGE.php index d435e89b..33283cdd 100644 --- a/tests/data/Calculation/Statistical/AVERAGE.php +++ b/tests/data/Calculation/Statistical/AVERAGE.php @@ -50,4 +50,8 @@ return [ '#VALUE!', [1, '2', 3.4, true, 5, null, 6.7, 'STRING', ''], ], + [ + '#DIV/0!', + [], + ], ]; diff --git a/tests/data/Calculation/Statistical/AVERAGEA.php b/tests/data/Calculation/Statistical/AVERAGEA.php index de6bf1b4..14781550 100644 --- a/tests/data/Calculation/Statistical/AVERAGEA.php +++ b/tests/data/Calculation/Statistical/AVERAGEA.php @@ -21,4 +21,12 @@ return [ 0.5, [true, false], ], + [ + 0.666666666667, + [true, false, 1], + ], + [ + '#DIV/0!', + [], + ], ]; diff --git a/tests/data/Calculation/Statistical/STDEV.php b/tests/data/Calculation/Statistical/STDEV.php index 846f8c71..30d9dc3b 100644 --- a/tests/data/Calculation/Statistical/STDEV.php +++ b/tests/data/Calculation/Statistical/STDEV.php @@ -9,4 +9,12 @@ return [ '#DIV/0!', ['A', 'B', 'C'], ], + [ + '#DIV/0!', + [true, false], + ], + [ + '#DIV/0!', + [true, false, 1], + ], ]; diff --git a/tests/data/Calculation/Statistical/STDEVA.php b/tests/data/Calculation/Statistical/STDEVA.php index 9fd45d65..13cecc61 100644 --- a/tests/data/Calculation/Statistical/STDEVA.php +++ b/tests/data/Calculation/Statistical/STDEVA.php @@ -9,4 +9,12 @@ return [ '#DIV/0!', [], ], + [ + 0.707106781187, + [true, false], + ], + [ + 0.577350269190, + [true, false, 1], + ], ]; diff --git a/tests/data/Calculation/Statistical/STDEVA_ODS.php b/tests/data/Calculation/Statistical/STDEVA_ODS.php new file mode 100644 index 00000000..559798e4 --- /dev/null +++ b/tests/data/Calculation/Statistical/STDEVA_ODS.php @@ -0,0 +1,20 @@ + Date: Sat, 27 Mar 2021 22:04:05 +0100 Subject: [PATCH 68/89] Implementation of the CHITEST() statistical function (#1960) * Implementation of the CHITEST() statistical function * A couple of additional edge case tests (rows = 1, columns = 1) --- CHANGELOG.md | 1 + .../Calculation/Calculation.php | 4 +- .../Statistical/Distributions/ChiSquared.php | 41 +++++++++++++++++++ .../Functions/Statistical/ChiTestTest.php | 27 ++++++++++++ .../data/Calculation/Statistical/CHITEST.php | 39 ++++++++++++++++++ 5 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiTestTest.php create mode 100644 tests/data/Calculation/Statistical/CHITEST.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b46323f..d6d252d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added +- Implemented the CHITEST() Statistical function. - Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908) ### Changed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 6e98fcb0..62524c37 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -518,12 +518,12 @@ class Calculation ], 'CHITEST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'], 'argumentCount' => '2', ], 'CHISQ.TEST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'test'], 'argumentCount' => '2', ], 'CHOOSE' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 636189b5..dfd090de 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -82,4 +82,45 @@ class ChiSquared return $newtonRaphson->execute($probability); } + + public static function test($actual, $expected) + { + $rows = count($actual); + $actual = Functions::flattenArray($actual); + $expected = Functions::flattenArray($expected); + $columns = count($actual) / $rows; + + $countActuals = count($actual); + $countExpected = count($expected); + if ($countActuals !== $countExpected || $countActuals === 1) { + return Functions::NAN(); + } + + $result = 0.0; + for ($i = 0; $i < $countActuals; ++$i) { + if ($expected[$i] == 0.0) { + return Functions::DIV0(); + } elseif ($expected[$i] < 0.0) { + return Functions::NAN(); + } + $result += (($actual[$i] - $expected[$i]) ** 2) / $expected[$i]; + } + + $degrees = self::degrees($rows, $columns); + + $result = self::distribution($result, $degrees); + + return $result; + } + + protected static function degrees(int $rows, int $columns): int + { + if ($rows === 1) { + return $columns - 1; + } elseif ($columns === 1) { + return $rows - 1; + } + + return ($columns - 1) * ($rows - 1); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiTestTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiTestTest.php new file mode 100644 index 00000000..5f9f361f --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiTestTest.php @@ -0,0 +1,27 @@ + Date: Sun, 28 Mar 2021 13:48:34 +0900 Subject: [PATCH 69/89] Update PHP deps Simplify our constraints thanks to PHPUnit 8.5 that supports PHP 8+ --- .github/workflows/main.yml | 2 +- composer.json | 32 +- composer.lock | 1097 +++++++++++++----------------------- 3 files changed, 414 insertions(+), 717 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 707a9a3f..ce38f646 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -43,7 +43,7 @@ jobs: - name: Delete composer lock file id: composer-lock - if: ${{ matrix.php-version == '8.0' || matrix.php-version == '8.1' }} + if: ${{ matrix.php-version == '8.1' }} run: | rm composer.lock echo "::set-output name=flags::--ignore-platform-reqs" diff --git a/composer.json b/composer.json index 483c3828..3b4ed556 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,19 @@ { "name": "phpoffice/phpspreadsheet", "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", - "keywords": ["PHP", "OpenXML", "Excel", "xlsx", "xls", "ods", "gnumeric", "spreadsheet"], + "keywords": [ + "PHP", + "OpenXML", + "Excel", + "xlsx", + "xls", + "ods", + "gnumeric", + "spreadsheet" + ], + "config": { + "sort-packages": true + }, "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", "type": "library", "license": "MIT", @@ -39,35 +51,35 @@ ] }, "require": { - "php": "^7.2||^8.0", + "php": "^7.2 || ^8.0", + "ext-simplexml": "*", "ext-ctype": "*", "ext-dom": "*", + "ext-fileinfo": "*", "ext-gd": "*", "ext-iconv": "*", - "ext-fileinfo": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-SimpleXML": "*", "ext-xml": "*", "ext-xmlreader": "*", "ext-xmlwriter": "*", "ext-zip": "*", "ext-zlib": "*", + "ezyang/htmlpurifier": "^4.13", "maennchen/zipstream-php": "^2.1", - "markbaker/complex": "^1.5||^2.0", - "markbaker/matrix": "^1.2||^2.0", - "psr/simple-cache": "^1.0", + "markbaker/complex": "^2.0", + "markbaker/matrix": "^2.0", "psr/http-client": "^1.0", "psr/http-factory": "^1.0", - "ezyang/htmlpurifier": "^4.13" + "psr/simple-cache": "^1.0" }, "require-dev": { - "dompdf/dompdf": "^0.8.5", + "dompdf/dompdf": "^1.0", "friendsofphp/php-cs-fixer": "^2.18", "jpgraph/jpgraph": "^4.0", "mpdf/mpdf": "^8.0", "phpcompatibility/php-compatibility": "^9.3", - "phpunit/phpunit": "^8.5||^9.3", + "phpunit/phpunit": "^8.5", "squizlabs/php_codesniffer": "^3.5", "tecnickcom/tcpdf": "^6.3" }, diff --git a/composer.lock b/composer.lock index bbb9ff7e..3f6efb00 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "8a567f9030fc836c175557d0430f1057", + "content-hash": "6c8f34baf3385a533fade30a9a9ad6f1", "packages": [ { "name": "ezyang/htmlpurifier", @@ -577,16 +577,16 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13" + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", - "reference": "f377a3dd1fde44d37b9831d68dc8dea3ffd28e13", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", + "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", "shasum": "" }, "require": { @@ -637,7 +637,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1" }, "funding": [ { @@ -653,7 +653,7 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-01-22T09:19:47+00:00" } ], "packages-dev": [ @@ -740,16 +740,16 @@ }, { "name": "composer/xdebug-handler", - "version": "1.4.5", + "version": "1.4.6", "source": { "type": "git", "url": "https://github.com/composer/xdebug-handler.git", - "reference": "f28d44c286812c714741478d968104c5e604a1d4" + "reference": "f27e06cd9675801df441b3656569b328e04aa37c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f28d44c286812c714741478d968104c5e604a1d4", - "reference": "f28d44c286812c714741478d968104c5e604a1d4", + "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/f27e06cd9675801df441b3656569b328e04aa37c", + "reference": "f27e06cd9675801df441b3656569b328e04aa37c", "shasum": "" }, "require": { @@ -757,7 +757,8 @@ "psr/log": "^1.0" }, "require-dev": { - "phpunit/phpunit": "^4.8.35 || ^5.7 || 6.5 - 8" + "phpstan/phpstan": "^0.12.55", + "symfony/phpunit-bridge": "^4.2 || ^5" }, "type": "library", "autoload": { @@ -783,7 +784,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/1.4.5" + "source": "https://github.com/composer/xdebug-handler/tree/1.4.6" }, "funding": [ { @@ -799,20 +800,20 @@ "type": "tidelift" } ], - "time": "2020-11-13T08:04:11+00:00" + "time": "2021-03-25T17:01:18+00:00" }, { "name": "doctrine/annotations", - "version": "1.11.1", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad" + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/ce77a7ba1770462cd705a91a151b6c3746f9c6ad", - "reference": "ce77a7ba1770462cd705a91a151b6c3746f9c6ad", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/b17c5014ef81d212ac539f07a1001832df1b6d3b", + "reference": "b17c5014ef81d212ac539f07a1001832df1b6d3b", "shasum": "" }, "require": { @@ -827,11 +828,6 @@ "phpunit/phpunit": "^7.5 || ^9.1.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.11.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" @@ -872,9 +868,9 @@ ], "support": { "issues": "https://github.com/doctrine/annotations/issues", - "source": "https://github.com/doctrine/annotations/tree/1.11.1" + "source": "https://github.com/doctrine/annotations/tree/1.12.1" }, - "time": "2020-10-26T10:28:16+00:00" + "time": "2021-02-21T21:00:45+00:00" }, { "name": "doctrine/instantiator", @@ -1027,16 +1023,16 @@ }, { "name": "dompdf/dompdf", - "version": "v0.8.6", + "version": "v1.0.2", "source": { "type": "git", "url": "https://github.com/dompdf/dompdf.git", - "reference": "db91d81866c69a42dad1d2926f61515a1e3f42c5" + "reference": "8768448244967a46d6e67b891d30878e0e15d25c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dompdf/dompdf/zipball/db91d81866c69a42dad1d2926f61515a1e3f42c5", - "reference": "db91d81866c69a42dad1d2926f61515a1e3f42c5", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/8768448244967a46d6e67b891d30878e0e15d25c", + "reference": "8768448244967a46d6e67b891d30878e0e15d25c", "shasum": "" }, "require": { @@ -1044,11 +1040,11 @@ "ext-mbstring": "*", "phenx/php-font-lib": "^0.5.2", "phenx/php-svg-lib": "^0.3.3", - "php": "^7.1" + "php": "^7.1 || ^8.0" }, "require-dev": { "mockery/mockery": "^1.3", - "phpunit/phpunit": "^7.5", + "phpunit/phpunit": "^7.5 || ^8 || ^9", "squizlabs/php_codesniffer": "^3.5" }, "suggest": { @@ -1093,22 +1089,22 @@ "homepage": "https://github.com/dompdf/dompdf", "support": { "issues": "https://github.com/dompdf/dompdf/issues", - "source": "https://github.com/dompdf/dompdf/tree/master" + "source": "https://github.com/dompdf/dompdf/tree/v1.0.2" }, - "time": "2020-08-30T22:54:22+00:00" + "time": "2021-01-08T14:18:52+00:00" }, { "name": "friendsofphp/php-cs-fixer", - "version": "v2.18.2", + "version": "v2.18.4", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "18f8c9d184ba777380794a389fabc179896ba913" + "reference": "06f764e3cb6d60822d8f5135205f9d32b5508a31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/18f8c9d184ba777380794a389fabc179896ba913", - "reference": "18f8c9d184ba777380794a389fabc179896ba913", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/06f764e3cb6d60822d8f5135205f9d32b5508a31", + "reference": "06f764e3cb6d60822d8f5135205f9d32b5508a31", "shasum": "" }, "require": { @@ -1170,6 +1166,7 @@ "tests/Test/IntegrationCaseFactoryInterface.php", "tests/Test/InternalIntegrationCaseFactory.php", "tests/Test/IsIdenticalConstraint.php", + "tests/Test/TokensWithObservedTransformers.php", "tests/TestCase.php" ] }, @@ -1190,7 +1187,7 @@ "description": "A tool to automatically fix PHP code style", "support": { "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues", - "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.2" + "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v2.18.4" }, "funding": [ { @@ -1198,7 +1195,7 @@ "type": "github" } ], - "time": "2021-01-26T00:22:21+00:00" + "time": "2021-03-20T14:52:33+00:00" }, { "name": "jpgraph/jpgraph", @@ -1378,62 +1375,6 @@ ], "time": "2020-11-13T09:40:50+00:00" }, - { - "name": "nikic/php-parser", - "version": "v4.10.4", - "source": { - "type": "git", - "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/c6d052fc58cb876152f89f532b95a8d7907e7f0e", - "reference": "c6d052fc58cb876152f89f532b95a8d7907e7f0e", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=7.0" - }, - "require-dev": { - "ircmaxell/php-yacc": "^0.0.7", - "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" - }, - "bin": [ - "bin/php-parse" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.9-dev" - } - }, - "autoload": { - "psr-4": { - "PhpParser\\": "lib/PhpParser" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Nikita Popov" - } - ], - "description": "A PHP parser written in PHP", - "keywords": [ - "parser", - "php" - ], - "support": { - "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v4.10.4" - }, - "time": "2020-12-20T10:01:03+00:00" - }, { "name": "paragonie/random_compat", "version": "v9.99.100", @@ -1546,16 +1487,16 @@ }, { "name": "phar-io/version", - "version": "3.0.4", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/phar-io/version.git", - "reference": "e4782611070e50613683d2b9a57730e9a3ba5451" + "reference": "bae7c545bef187884426f042434e561ab1ddb182" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phar-io/version/zipball/e4782611070e50613683d2b9a57730e9a3ba5451", - "reference": "e4782611070e50613683d2b9a57730e9a3ba5451", + "url": "https://api.github.com/repos/phar-io/version/zipball/bae7c545bef187884426f042434e561ab1ddb182", + "reference": "bae7c545bef187884426f042434e561ab1ddb182", "shasum": "" }, "require": { @@ -1591,9 +1532,9 @@ "description": "Library for handling version information and constraints", "support": { "issues": "https://github.com/phar-io/version/issues", - "source": "https://github.com/phar-io/version/tree/3.0.4" + "source": "https://github.com/phar-io/version/tree/3.1.0" }, - "time": "2020-12-13T23:18:30+00:00" + "time": "2021-02-23T14:00:09+00:00" }, { "name": "phenx/php-font-lib", @@ -1957,16 +1898,16 @@ }, { "name": "phpspec/prophecy", - "version": "1.12.2", + "version": "1.13.0", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "245710e971a030f42e08f4912863805570f23d39" + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/245710e971a030f42e08f4912863805570f23d39", - "reference": "245710e971a030f42e08f4912863805570f23d39", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/be1996ed8adc35c3fd795488a653f4b518be70ea", + "reference": "be1996ed8adc35c3fd795488a653f4b518be70ea", "shasum": "" }, "require": { @@ -2018,50 +1959,46 @@ ], "support": { "issues": "https://github.com/phpspec/prophecy/issues", - "source": "https://github.com/phpspec/prophecy/tree/1.12.2" + "source": "https://github.com/phpspec/prophecy/tree/1.13.0" }, - "time": "2020-12-19T10:15:11+00:00" + "time": "2021-03-17T13:42:18+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.5", + "version": "7.0.14", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1" + "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f3e026641cc91909d421802dd3ac7827ebfd97e1", - "reference": "f3e026641cc91909d421802dd3ac7827ebfd97e1", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/bb7c9a210c72e4709cdde67f8b7362f672f2225c", + "reference": "bb7c9a210c72e4709cdde67f8b7362f672f2225c", "shasum": "" }, "require": { "ext-dom": "*", - "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.10.2", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.3", - "phpunit/php-text-template": "^2.0.2", - "sebastian/code-unit-reverse-lookup": "^2.0.2", - "sebastian/complexity": "^2.0", - "sebastian/environment": "^5.1.2", - "sebastian/lines-of-code": "^1.0.3", - "sebastian/version": "^3.0.1", - "theseer/tokenizer": "^1.2.0" + "php": ">=7.2", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^3.1.1 || ^4.0", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^4.2.2", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1.3" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.2.2" }, "suggest": { - "ext-pcov": "*", - "ext-xdebug": "*" + "ext-xdebug": "^2.7.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "9.2-dev" + "dev-master": "7.0-dev" } }, "autoload": { @@ -2089,7 +2026,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", - "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.5" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/7.0.14" }, "funding": [ { @@ -2097,32 +2034,32 @@ "type": "github" } ], - "time": "2020-11-28T06:44:49+00:00" + "time": "2020-12-02T13:39:03+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.5", + "version": "2.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8" + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/aa4be8575f26070b100fccb67faabb28f21f66f8", - "reference": "aa4be8575f26070b100fccb67faabb28f21f66f8", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/4b49fb70f067272b659ef0174ff9ca40fdaa6357", + "reference": "4b49fb70f067272b659ef0174ff9ca40fdaa6357", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -2149,7 +2086,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.5" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/2.0.3" }, "funding": [ { @@ -2157,97 +2094,26 @@ "type": "github" } ], - "time": "2020-09-28T05:57:25+00:00" - }, - { - "name": "phpunit/php-invoker", - "version": "3.1.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-pcntl": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.1-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Invoke callables with a timeout", - "homepage": "https://github.com/sebastianbergmann/php-invoker/", - "keywords": [ - "process" - ], - "support": { - "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2020-11-30T08:25:21+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "1.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" + "php": ">=5.3.3" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, "autoload": { "classmap": [ "src/" @@ -2271,40 +2137,34 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/1.2.1" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2015-06-21T13:50:34+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "2.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/2454ae1765516d20c4ffe103d85a58a9a3bd5662", + "reference": "2454ae1765516d20c4ffe103d85a58a9a3bd5662", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "2.1-dev" } }, "autoload": { @@ -2330,7 +2190,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "source": "https://github.com/sebastianbergmann/php-timer/tree/2.1.3" }, "funding": [ { @@ -2338,20 +2198,80 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2020-11-30T08:20:02+00:00" }, { - "name": "phpunit/phpunit", - "version": "9.5.1", + "name": "phpunit/php-token-stream", + "version": "3.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "e7bdf4085de85a825f4424eae52c99a1cec2f360" + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/e7bdf4085de85a825f4424eae52c99a1cec2f360", - "reference": "e7bdf4085de85a825f4424eae52c99a1cec2f360", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/472b687829041c24b25f475e14c2f38a09edf1c2", + "reference": "472b687829041c24b25f475e14c2f38a09edf1c2", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=7.1" + }, + "require-dev": { + "phpunit/phpunit": "^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-token-stream/issues", + "source": "https://github.com/sebastianbergmann/php-token-stream/tree/3.1.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "abandoned": true, + "time": "2020-11-30T08:38:46+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "8.5.15", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "038d4196d8e8cb405cd5e82cedfe413ad6eef9ef" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/038d4196d8e8cb405cd5e82cedfe413ad6eef9ef", + "reference": "038d4196d8e8cb405cd5e82cedfe413ad6eef9ef", "shasum": "" }, "require": { @@ -2362,35 +2282,32 @@ "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.10.1", + "myclabs/deep-copy": "^1.10.0", "phar-io/manifest": "^2.0.1", "phar-io/version": "^3.0.2", - "php": ">=7.3", - "phpspec/prophecy": "^1.12.1", - "phpunit/php-code-coverage": "^9.2.3", - "phpunit/php-file-iterator": "^3.0.5", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.3", - "phpunit/php-timer": "^5.0.2", - "sebastian/cli-parser": "^1.0.1", - "sebastian/code-unit": "^1.0.6", - "sebastian/comparator": "^4.0.5", - "sebastian/diff": "^4.0.3", - "sebastian/environment": "^5.1.3", - "sebastian/exporter": "^4.0.3", - "sebastian/global-state": "^5.0.1", - "sebastian/object-enumerator": "^4.0.3", - "sebastian/resource-operations": "^3.0.3", - "sebastian/type": "^2.3", - "sebastian/version": "^3.0.2" + "php": ">=7.2", + "phpspec/prophecy": "^1.10.3", + "phpunit/php-code-coverage": "^7.0.12", + "phpunit/php-file-iterator": "^2.0.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^2.1.2", + "sebastian/comparator": "^3.0.2", + "sebastian/diff": "^3.0.2", + "sebastian/environment": "^4.2.3", + "sebastian/exporter": "^3.1.2", + "sebastian/global-state": "^3.0.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^2.0.1", + "sebastian/type": "^1.1.3", + "sebastian/version": "^2.0.1" }, "require-dev": { - "ext-pdo": "*", - "phpspec/prophecy-phpunit": "^2.0.1" + "ext-pdo": "*" }, "suggest": { "ext-soap": "*", - "ext-xdebug": "*" + "ext-xdebug": "*", + "phpunit/php-invoker": "^2.0.0" }, "bin": [ "phpunit" @@ -2398,15 +2315,12 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.5-dev" + "dev-master": "8.5-dev" } }, "autoload": { "classmap": [ "src/" - ], - "files": [ - "src/Framework/Assert/Functions.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -2429,7 +2343,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.1" + "source": "https://github.com/sebastianbergmann/phpunit/tree/8.5.15" }, "funding": [ { @@ -2441,31 +2355,26 @@ "type": "github" } ], - "time": "2021-01-17T07:42:25+00:00" + "time": "2021-03-17T07:27:54+00:00" }, { "name": "psr/container", - "version": "1.0.0", + "version": "1.1.1", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", - "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", + "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", "shasum": "" }, "require": { - "php": ">=5.3.0" + "php": ">=7.2.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, "autoload": { "psr-4": { "Psr\\Container\\": "src/" @@ -2478,7 +2387,7 @@ "authors": [ { "name": "PHP-FIG", - "homepage": "http://www.php-fig.org/" + "homepage": "https://www.php-fig.org/" } ], "description": "Common Container Interface (PHP FIG PSR-11)", @@ -2492,9 +2401,9 @@ ], "support": { "issues": "https://github.com/php-fig/container/issues", - "source": "https://github.com/php-fig/container/tree/master" + "source": "https://github.com/php-fig/container/tree/1.1.1" }, - "time": "2017-02-14T16:28:37+00:00" + "time": "2021-03-05T17:36:06+00:00" }, { "name": "psr/event-dispatcher", @@ -2645,142 +2554,30 @@ }, "time": "2020-06-01T09:10:00+00:00" }, - { - "name": "sebastian/cli-parser", - "version": "1.0.1", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "reference": "442e7c7e687e42adc03470c7b668bc4b2402c0b2", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for parsing CLI options", - "homepage": "https://github.com/sebastianbergmann/cli-parser", - "support": { - "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.1" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:08:49+00:00" - }, - { - "name": "sebastian/code-unit", - "version": "1.0.8", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", - "shasum": "" - }, - "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Collection of value objects that represent the PHP code units", - "homepage": "https://github.com/sebastianbergmann/code-unit", - "support": { - "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T13:08:54+00:00" - }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "1.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/1de8cd5c010cb153fcd68b8d0f64606f523f7619", + "reference": "1de8cd5c010cb153fcd68b8d0f64606f523f7619", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.6" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.0.x-dev" } }, "autoload": { @@ -2802,7 +2599,7 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/1.0.2" }, "funding": [ { @@ -2810,34 +2607,34 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2020-11-30T08:15:22+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.6", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382" + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55f4261989e546dc112258c7a75935a81a7ce382", - "reference": "55f4261989e546dc112258c7a75935a81a7ce382", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/1071dfcef776a57013124ff35e1fc41ccd294758", + "reference": "1071dfcef776a57013124ff35e1fc41ccd294758", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "php": ">=7.1", + "sebastian/diff": "^3.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2876,7 +2673,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.6" + "source": "https://github.com/sebastianbergmann/comparator/tree/3.0.3" }, "funding": [ { @@ -2884,90 +2681,33 @@ "type": "github" } ], - "time": "2020-10-26T15:49:45+00:00" - }, - { - "name": "sebastian/complexity", - "version": "2.0.2", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/739b35e53379900cc9ac327b2147867b8b6efd88", - "reference": "739b35e53379900cc9ac327b2147867b8b6efd88", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.7", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for calculating the complexity of PHP code units", - "homepage": "https://github.com/sebastianbergmann/complexity", - "support": { - "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.2" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-10-26T15:52:27+00:00" + "time": "2020-11-30T08:04:30+00:00" }, { "name": "sebastian/diff", - "version": "4.0.4", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d" + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/3461e3fccc7cfdfc2720be910d3bd73c69be590d", - "reference": "3461e3fccc7cfdfc2720be910d3bd73c69be590d", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/14f72dd46eaf2f2293cbe79c93cc0bc43161a211", + "reference": "14f72dd46eaf2f2293cbe79c93cc0bc43161a211", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3", - "symfony/process": "^4.2 || ^5" + "phpunit/phpunit": "^7.5 || ^8.0", + "symfony/process": "^2 || ^3.3 || ^4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -2999,7 +2739,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/diff/tree/3.0.3" }, "funding": [ { @@ -3007,27 +2747,27 @@ "type": "github" } ], - "time": "2020-10-26T13:10:38+00:00" + "time": "2020-11-30T07:59:04+00:00" }, { "name": "sebastian/environment", - "version": "5.1.3", + "version": "4.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac" + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/388b6ced16caa751030f6a69e588299fa09200ac", - "reference": "388b6ced16caa751030f6a69e588299fa09200ac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", + "reference": "d47bbbad83711771f167c72d4e3f25f7fcc1f8b0", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.1" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^7.5" }, "suggest": { "ext-posix": "*" @@ -3035,7 +2775,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-master": "4.2-dev" } }, "autoload": { @@ -3062,7 +2802,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.3" + "source": "https://github.com/sebastianbergmann/environment/tree/4.2.4" }, "funding": [ { @@ -3070,34 +2810,34 @@ "type": "github" } ], - "time": "2020-09-28T05:52:38+00:00" + "time": "2020-11-30T07:53:42+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.3", + "version": "3.1.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65" + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/d89cc98761b8cb5a1a235a6b703ae50d34080e65", - "reference": "d89cc98761b8cb5a1a235a6b703ae50d34080e65", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/6b853149eab67d4da22291d36f5b0631c0fd856e", + "reference": "6b853149eab67d4da22291d36f5b0631c0fd856e", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "php": ">=7.0", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -3139,7 +2879,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.3" + "source": "https://github.com/sebastianbergmann/exporter/tree/3.1.3" }, "funding": [ { @@ -3147,30 +2887,30 @@ "type": "github" } ], - "time": "2020-09-28T05:24:23+00:00" + "time": "2020-11-30T07:47:53+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.2", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455" + "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/a90ccbddffa067b51f574dea6eb25d5680839455", - "reference": "a90ccbddffa067b51f574dea6eb25d5680839455", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/474fb9edb7ab891665d3bfc6317f42a0a150454b", + "reference": "474fb9edb7ab891665d3bfc6317f42a0a150454b", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=7.2", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.0" }, "suggest": { "ext-uopz": "*" @@ -3178,7 +2918,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "3.0-dev" } }, "autoload": { @@ -3203,7 +2943,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.2" + "source": "https://github.com/sebastianbergmann/global-state/tree/3.0.1" }, "funding": [ { @@ -3211,91 +2951,34 @@ "type": "github" } ], - "time": "2020-10-26T15:55:19+00:00" - }, - { - "name": "sebastian/lines-of-code", - "version": "1.0.3", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "reference": "c1c2e997aa3146983ed888ad08b15470a2e22ecc", - "shasum": "" - }, - "require": { - "nikic/php-parser": "^4.6", - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "Library for counting the lines of code in PHP source code", - "homepage": "https://github.com/sebastianbergmann/lines-of-code", - "support": { - "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.3" - }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-11-28T06:42:11+00:00" + "time": "2020-11-30T07:43:24+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "3.0.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", + "reference": "e67f6d32ebd0c749cf9d1dbd9f226c727043cdf2", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -3317,7 +3000,7 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/3.0.4" }, "funding": [ { @@ -3325,32 +3008,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2020-11-30T07:40:27+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", + "reference": "9b8772b9cbd456ab45d4a598d2dd1a1bced6363d", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -3372,7 +3055,7 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/1.1.2" }, "funding": [ { @@ -3380,32 +3063,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2020-11-30T07:37:18+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172" + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/cd9d8cf3c5804de4341c283ed787f099f5506172", - "reference": "cd9d8cf3c5804de4341c283ed787f099f5506172", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/367dcba38d6e1977be014dc4b22f47a484dac7fb", + "reference": "367dcba38d6e1977be014dc4b22f47a484dac7fb", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -3435,7 +3118,7 @@ "homepage": "http://www.github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.4" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/3.0.1" }, "funding": [ { @@ -3443,32 +3126,29 @@ "type": "github" } ], - "time": "2020-10-26T13:17:30+00:00" + "time": "2020-11-30T07:34:24+00:00" }, { "name": "sebastian/resource-operations", - "version": "3.0.3", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8" + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", - "reference": "0f4443cb3a1d92ce809899753bc0d5d5a8dd19a8", + "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/31d35ca87926450c44eae7e2611d45a7a65ea8b3", + "reference": "31d35ca87926450c44eae7e2611d45a7a65ea8b3", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.0" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -3490,7 +3170,7 @@ "homepage": "https://www.github.com/sebastianbergmann/resource-operations", "support": { "issues": "https://github.com/sebastianbergmann/resource-operations/issues", - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.3" + "source": "https://github.com/sebastianbergmann/resource-operations/tree/2.0.2" }, "funding": [ { @@ -3498,32 +3178,32 @@ "type": "github" } ], - "time": "2020-09-28T06:45:17+00:00" + "time": "2020-11-30T07:30:19+00:00" }, { "name": "sebastian/type", - "version": "2.3.1", + "version": "1.1.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2" + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/81cd61ab7bbf2de744aba0ea61fae32f721df3d2", - "reference": "81cd61ab7bbf2de744aba0ea61fae32f721df3d2", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0150cfbc4495ed2df3872fb31b26781e4e077eb4", + "reference": "0150cfbc4495ed2df3872fb31b26781e4e077eb4", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=7.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "1.1-dev" } }, "autoload": { @@ -3546,7 +3226,7 @@ "homepage": "https://github.com/sebastianbergmann/type", "support": { "issues": "https://github.com/sebastianbergmann/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/2.3.1" + "source": "https://github.com/sebastianbergmann/type/tree/1.1.4" }, "funding": [ { @@ -3554,29 +3234,29 @@ "type": "github" } ], - "time": "2020-10-26T13:18:59+00:00" + "time": "2020-11-30T07:25:11+00:00" }, { "name": "sebastian/version", - "version": "3.0.2", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", + "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=5.6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { @@ -3599,28 +3279,22 @@ "homepage": "https://github.com/sebastianbergmann/version", "support": { "issues": "https://github.com/sebastianbergmann/version/issues", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "source": "https://github.com/sebastianbergmann/version/tree/master" }, - "funding": [ - { - "url": "https://github.com/sebastianbergmann", - "type": "github" - } - ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2016-10-03T07:35:21+00:00" }, { "name": "setasign/fpdi", - "version": "v2.3.5", + "version": "v2.3.6", "source": { "type": "git", "url": "https://github.com/Setasign/FPDI.git", - "reference": "f2246c8669bd25834f5c264425eb0e250d7a9312" + "reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Setasign/FPDI/zipball/f2246c8669bd25834f5c264425eb0e250d7a9312", - "reference": "f2246c8669bd25834f5c264425eb0e250d7a9312", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/6231e315f73e4f62d72b73f3d6d78ff0eed93c31", + "reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31", "shasum": "" }, "require": { @@ -3671,7 +3345,7 @@ ], "support": { "issues": "https://github.com/Setasign/FPDI/issues", - "source": "https://github.com/Setasign/FPDI/tree/v2.3.5" + "source": "https://github.com/Setasign/FPDI/tree/v2.3.6" }, "funding": [ { @@ -3679,7 +3353,7 @@ "type": "tidelift" } ], - "time": "2020-12-03T13:40:03+00:00" + "time": "2021-02-11T11:37:01+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -3739,16 +3413,16 @@ }, { "name": "symfony/console", - "version": "v5.2.2", + "version": "v5.2.5", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "d62ec79478b55036f65e2602e282822b8eaaff0a" + "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/d62ec79478b55036f65e2602e282822b8eaaff0a", - "reference": "d62ec79478b55036f65e2602e282822b8eaaff0a", + "url": "https://api.github.com/repos/symfony/console/zipball/938ebbadae1b0a9c9d1ec313f87f9708609f1b79", + "reference": "938ebbadae1b0a9c9d1ec313f87f9708609f1b79", "shasum": "" }, "require": { @@ -3816,7 +3490,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v5.2.2" + "source": "https://github.com/symfony/console/tree/v5.2.5" }, "funding": [ { @@ -3832,7 +3506,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:15:41+00:00" + "time": "2021-03-06T13:42:15+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3903,16 +3577,16 @@ }, { "name": "symfony/event-dispatcher", - "version": "v5.2.2", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367" + "reference": "d08d6ec121a425897951900ab692b612a61d6240" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/4f9760f8074978ad82e2ce854dff79a71fe45367", - "reference": "4f9760f8074978ad82e2ce854dff79a71fe45367", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d08d6ec121a425897951900ab692b612a61d6240", + "reference": "d08d6ec121a425897951900ab692b612a61d6240", "shasum": "" }, "require": { @@ -3968,7 +3642,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.2" + "source": "https://github.com/symfony/event-dispatcher/tree/v5.2.4" }, "funding": [ { @@ -3984,7 +3658,7 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:36:42+00:00" + "time": "2021-02-18T17:12:37+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4067,16 +3741,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.2.2", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038" + "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/262d033b57c73e8b59cd6e68a45c528318b15038", - "reference": "262d033b57c73e8b59cd6e68a45c528318b15038", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/710d364200997a5afde34d9fe57bd52f3cc1e108", + "reference": "710d364200997a5afde34d9fe57bd52f3cc1e108", "shasum": "" }, "require": { @@ -4109,7 +3783,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v5.2.2" + "source": "https://github.com/symfony/filesystem/tree/v5.2.4" }, "funding": [ { @@ -4125,20 +3799,20 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:01:46+00:00" + "time": "2021-02-12T10:38:38+00:00" }, { "name": "symfony/finder", - "version": "v5.2.2", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "196f45723b5e618bf0e23b97e96d11652696ea9e" + "reference": "0d639a0943822626290d169965804f79400e6a04" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/196f45723b5e618bf0e23b97e96d11652696ea9e", - "reference": "196f45723b5e618bf0e23b97e96d11652696ea9e", + "url": "https://api.github.com/repos/symfony/finder/zipball/0d639a0943822626290d169965804f79400e6a04", + "reference": "0d639a0943822626290d169965804f79400e6a04", "shasum": "" }, "require": { @@ -4170,7 +3844,7 @@ "description": "Finds files and directories via an intuitive fluent interface", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/finder/tree/v5.2.2" + "source": "https://github.com/symfony/finder/tree/v5.2.4" }, "funding": [ { @@ -4186,11 +3860,11 @@ "type": "tidelift" } ], - "time": "2021-01-27T10:01:46+00:00" + "time": "2021-02-15T18:55:04+00:00" }, { "name": "symfony/options-resolver", - "version": "v5.2.2", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/options-resolver.git", @@ -4239,7 +3913,7 @@ "options" ], "support": { - "source": "https://github.com/symfony/options-resolver/tree/v5.2.2" + "source": "https://github.com/symfony/options-resolver/tree/v5.2.4" }, "funding": [ { @@ -4259,7 +3933,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4318,7 +3992,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1" }, "funding": [ { @@ -4338,16 +4012,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "267a9adeb8ecb8071040a740930e077cdfb987af" + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/267a9adeb8ecb8071040a740930e077cdfb987af", - "reference": "267a9adeb8ecb8071040a740930e077cdfb987af", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", + "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", "shasum": "" }, "require": { @@ -4399,7 +4073,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.22.1" }, "funding": [ { @@ -4415,20 +4089,20 @@ "type": "tidelift" } ], - "time": "2021-01-07T16:49:33+00:00" + "time": "2021-01-22T09:19:47+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "6e971c891537eb617a00bb07a43d182a6915faba" + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/6e971c891537eb617a00bb07a43d182a6915faba", - "reference": "6e971c891537eb617a00bb07a43d182a6915faba", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", + "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", "shasum": "" }, "require": { @@ -4483,7 +4157,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.22.1" }, "funding": [ { @@ -4499,7 +4173,7 @@ "type": "tidelift" } ], - "time": "2021-01-07T17:09:11+00:00" + "time": "2021-01-22T09:19:47+00:00" }, { "name": "symfony/polyfill-php70", @@ -4571,7 +4245,7 @@ }, { "name": "symfony/polyfill-php72", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", @@ -4627,7 +4301,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-php72/tree/v1.22.1" }, "funding": [ { @@ -4647,7 +4321,7 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", @@ -4706,7 +4380,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-php73/tree/v1.22.1" }, "funding": [ { @@ -4726,7 +4400,7 @@ }, { "name": "symfony/polyfill-php80", - "version": "v1.22.0", + "version": "v1.22.1", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", @@ -4789,7 +4463,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1" }, "funding": [ { @@ -4809,7 +4483,7 @@ }, { "name": "symfony/process", - "version": "v5.2.2", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/process.git", @@ -4851,7 +4525,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v5.2.2" + "source": "https://github.com/symfony/process/tree/v5.2.4" }, "funding": [ { @@ -4950,7 +4624,7 @@ }, { "name": "symfony/stopwatch", - "version": "v5.2.2", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", @@ -4992,7 +4666,7 @@ "description": "Provides a way to profile code", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/stopwatch/tree/v5.2.2" + "source": "https://github.com/symfony/stopwatch/tree/v5.2.4" }, "funding": [ { @@ -5012,16 +4686,16 @@ }, { "name": "symfony/string", - "version": "v5.2.2", + "version": "v5.2.4", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "c95468897f408dd0aca2ff582074423dd0455122" + "reference": "4e78d7d47061fa183639927ec40d607973699609" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/c95468897f408dd0aca2ff582074423dd0455122", - "reference": "c95468897f408dd0aca2ff582074423dd0455122", + "url": "https://api.github.com/repos/symfony/string/zipball/4e78d7d47061fa183639927ec40d607973699609", + "reference": "4e78d7d47061fa183639927ec40d607973699609", "shasum": "" }, "require": { @@ -5075,7 +4749,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v5.2.2" + "source": "https://github.com/symfony/string/tree/v5.2.4" }, "funding": [ { @@ -5091,20 +4765,20 @@ "type": "tidelift" } ], - "time": "2021-01-25T15:14:59+00:00" + "time": "2021-02-16T10:20:28+00:00" }, { "name": "tecnickcom/tcpdf", - "version": "6.3.5", + "version": "6.4.1", "source": { "type": "git", "url": "https://github.com/tecnickcom/TCPDF.git", - "reference": "19a535eaa7fb1c1cac499109deeb1a7a201b4549" + "reference": "5ba838befdb37ef06a16d9f716f35eb03cb1b329" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/19a535eaa7fb1c1cac499109deeb1a7a201b4549", - "reference": "19a535eaa7fb1c1cac499109deeb1a7a201b4549", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/5ba838befdb37ef06a16d9f716f35eb03cb1b329", + "reference": "5ba838befdb37ef06a16d9f716f35eb03cb1b329", "shasum": "" }, "require": { @@ -5155,9 +4829,15 @@ ], "support": { "issues": "https://github.com/tecnickcom/TCPDF/issues", - "source": "https://github.com/tecnickcom/TCPDF/tree/6.3.5" + "source": "https://github.com/tecnickcom/TCPDF/tree/6.4.1" }, - "time": "2020-02-14T14:20:12+00:00" + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", + "type": "custom" + } + ], + "time": "2021-03-27T16:00:33+00:00" }, { "name": "theseer/tokenizer", @@ -5211,30 +4891,35 @@ }, { "name": "webmozart/assert", - "version": "1.9.1", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/webmozarts/assert.git", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", - "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/6964c76c7804814a842473e0c8fd15bab0f18e25", + "reference": "6964c76c7804814a842473e0c8fd15bab0f18e25", "shasum": "" }, "require": { - "php": "^5.3.3 || ^7.0 || ^8.0", + "php": "^7.2 || ^8.0", "symfony/polyfill-ctype": "^1.8" }, "conflict": { "phpstan/phpstan": "<0.12.20", - "vimeo/psalm": "<3.9.1" + "vimeo/psalm": "<4.6.1 || 4.6.2" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^7.5.13" + "phpunit/phpunit": "^8.5.13" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, "autoload": { "psr-4": { "Webmozart\\Assert\\": "src/" @@ -5258,9 +4943,9 @@ ], "support": { "issues": "https://github.com/webmozarts/assert/issues", - "source": "https://github.com/webmozarts/assert/tree/1.9.1" + "source": "https://github.com/webmozarts/assert/tree/1.10.0" }, - "time": "2020-07-08T17:02:28+00:00" + "time": "2021-03-09T10:59:23+00:00" } ], "aliases": [], @@ -5269,15 +4954,15 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^7.2||^8.0", + "php": "^7.2 || ^8.0", + "ext-simplexml": "*", "ext-ctype": "*", "ext-dom": "*", + "ext-fileinfo": "*", "ext-gd": "*", "ext-iconv": "*", - "ext-fileinfo": "*", "ext-libxml": "*", "ext-mbstring": "*", - "ext-simplexml": "*", "ext-xml": "*", "ext-xmlreader": "*", "ext-xmlwriter": "*", From e2ff14fe89746a62984f54c68044466e2c80fb12 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 28 Mar 2021 16:13:00 +0200 Subject: [PATCH 70/89] Implemented the CHISQ.DIST() Statistical function. (#1961) * Implementation of the CHISQ.DIST() statistical function for left tail distribution --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 10 +-- .../Calculation/Statistical.php | 14 ++-- .../Statistical/Distributions/ChiSquared.php | 52 +++++++++++++-- .../Statistical/Distributions/GammaBase.php | 3 +- .../Distributions/NewtonRaphson.php | 2 +- ...ChiInvTest.php => ChiDistLeftTailTest.php} | 12 ++-- ...iDistTest.php => ChiDistRightTailTest.php} | 4 +- .../Statistical/ChiInvRightTailTest.php | 37 +++++++++++ .../Statistical/CHIDISTLeftTail.php | 64 +++++++++++++++++++ .../{CHIDIST.php => CHIDISTRightTail.php} | 0 .../{CHIINV.php => CHIINVRightTail.php} | 12 +++- 12 files changed, 183 insertions(+), 29 deletions(-) rename tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/{ChiInvTest.php => ChiDistLeftTailTest.php} (58%) rename tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/{ChiDistTest.php => ChiDistRightTailTest.php} (92%) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvRightTailTest.php create mode 100644 tests/data/Calculation/Statistical/CHIDISTLeftTail.php rename tests/data/Calculation/Statistical/{CHIDIST.php => CHIDISTRightTail.php} (100%) rename tests/data/Calculation/Statistical/{CHIINV.php => CHIINVRightTail.php} (82%) diff --git a/CHANGELOG.md b/CHANGELOG.md index d6d252d5..d7b123ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Implemented the CHITEST() Statistical function. +- Implemented the CHITEST() and CHISQ.DIST() Statistical function. - Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908) ### Changed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 62524c37..c4a88bc3 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -488,22 +488,22 @@ class Calculation ], 'CHIDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distribution'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'], 'argumentCount' => '2', ], 'CHISQ.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionLeftTail'], 'argumentCount' => '3', ], 'CHISQ.DIST.RT' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distribution'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'distributionRightTail'], 'argumentCount' => '2', ], 'CHIINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverse'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'], 'argumentCount' => '2', ], 'CHISQ.INV' => [ @@ -513,7 +513,7 @@ class Calculation ], 'CHISQ.INV.RT' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverse'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseRightTail'], 'argumentCount' => '2', ], 'CHITEST' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 5f557431..01caba14 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -297,8 +297,8 @@ class Statistical * * @Deprecated 1.18.0 * - * @see Statistical\Distributions\ChiSquared::distribution() - * Use the distribution() method in the Statistical\Distributions\ChiSquared class instead + * @see Statistical\Distributions\ChiSquared::distributionRightTail() + * Use the distributionRightTail() method in the Statistical\Distributions\ChiSquared class instead * * @param float $value Value for the function * @param float $degrees degrees of freedom @@ -307,7 +307,7 @@ class Statistical */ public static function CHIDIST($value, $degrees) { - return Statistical\Distributions\ChiSquared::distribution($value, $degrees); + return Statistical\Distributions\ChiSquared::distributionRightTail($value, $degrees); } /** @@ -317,8 +317,8 @@ class Statistical * * @Deprecated 1.18.0 * - * @see Statistical\Distributions\ChiSquared::inverse() - * Use the inverse() method in the Statistical\Distributions\ChiSquared class instead + * @see Statistical\Distributions\ChiSquared::inverseRightTail() + * Use the inverseRightTail() method in the Statistical\Distributions\ChiSquared class instead * * @param float $probability Probability for the function * @param float $degrees degrees of freedom @@ -327,7 +327,7 @@ class Statistical */ public static function CHIINV($probability, $degrees) { - return Statistical\Distributions\ChiSquared::inverse($probability, $degrees); + return Statistical\Distributions\ChiSquared::inverseRightTail($probability, $degrees); } /** @@ -2159,7 +2159,7 @@ class Statistical /** * TINV. * - * Returns the one-tailed probability of the chi-squared distribution. + * Returns the one-tailed probability of the Student-T distribution. * * @Deprecated 1.18.0 * diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index dfd090de..409e5883 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -21,7 +21,7 @@ class ChiSquared * * @return float|string */ - public static function distribution($value, $degrees) + public static function distributionRightTail($value, $degrees) { $value = Functions::flattenSingleValue($value); $degrees = Functions::flattenSingleValue($degrees); @@ -48,16 +48,60 @@ class ChiSquared } /** - * CHIINV. + * CHIDIST. * * Returns the one-tailed probability of the chi-squared distribution. * + * @param mixed (float) $value Value for the function + * @param mixed (int) $degrees degrees of freedom + * @param mixed $cumulative + * + * @return float|string + */ + public static function distributionLeftTail($value, $degrees, $cumulative) + { + $value = Functions::flattenSingleValue($value); + $degrees = Functions::flattenSingleValue($degrees); + $cumulative = Functions::flattenSingleValue($cumulative); + + try { + $value = self::validateFloat($value); + $degrees = self::validateInt($degrees); + $cumulative = self::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($degrees < 1) { + return Functions::NAN(); + } + if ($value < 0) { + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + return 1; + } + + return Functions::NAN(); + } + + if ($cumulative === true) { + return 1 - self::distributionRightTail($value, $degrees); + } + + return (($value ** (($degrees / 2) - 1) * exp(-$value / 2))) / + ((2 ** ($degrees / 2)) * Gamma::gammaValue($degrees / 2)); + } + + /** + * CHIINV. + * + * Returns the inverse of the right-tailed probability of the chi-squared distribution. + * * @param mixed (float) $probability Probability for the function * @param mixed (int) $degrees degrees of freedom * * @return float|string */ - public static function inverse($probability, $degrees) + public static function inverseRightTail($probability, $degrees) { $probability = Functions::flattenSingleValue($probability); $degrees = Functions::flattenSingleValue($degrees); @@ -108,7 +152,7 @@ class ChiSquared $degrees = self::degrees($rows, $columns); - $result = self::distribution($result, $degrees); + $result = self::distributionRightTail($result, $degrees); return $result; } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php index 3f76787d..89170f7c 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/GammaBase.php @@ -30,14 +30,15 @@ abstract class GammaBase $xLo = 0; $xHi = $alpha * $beta * 5; - $x = $xNew = 1; $dx = 1024; + $x = $xNew = 1; $i = 0; while ((abs($dx) > Functions::PRECISION) && (++$i <= self::MAX_ITERATIONS)) { // Apply Newton-Raphson step $result = self::calculateDistribution($x, $alpha, $beta, true); $error = $result - $probability; + if ($error == 0.0) { $dx = 0; } elseif ($error < 0.0) { diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php index 298cdfaf..d4025f6f 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php @@ -20,8 +20,8 @@ class NewtonRaphson $xLo = 100; $xHi = 0; - $x = $xNew = 1; $dx = 1; + $x = $xNew = 1; $i = 0; while ((abs($dx) > Functions::PRECISION) && ($i++ < self::MAX_ITERATIONS)) { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistLeftTailTest.php similarity index 58% rename from tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvTest.php rename to tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistLeftTailTest.php index 72680914..3c7a8d4e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistLeftTailTest.php @@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; -class ChiInvTest extends TestCase +class ChiDistLeftTailTest extends TestCase { protected function setUp(): void { @@ -14,18 +14,18 @@ class ChiInvTest extends TestCase } /** - * @dataProvider providerCHIINV + * @dataProvider providerCHIDIST * * @param mixed $expectedResult */ - public function testCHIINV($expectedResult, ...$args): void + public function testCHIDIST($expectedResult, ...$args): void { - $result = Statistical::CHIINV(...$args); + $result = Statistical\Distributions\ChiSquared::distributionLeftTail(...$args); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } - public function providerCHIINV() + public function providerCHIDIST() { - return require 'tests/data/Calculation/Statistical/CHIINV.php'; + return require 'tests/data/Calculation/Statistical/CHIDISTLeftTail.php'; } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistRightTailTest.php similarity index 92% rename from tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistTest.php rename to tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistRightTailTest.php index 9dc7326c..26bf5ab7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiDistRightTailTest.php @@ -6,7 +6,7 @@ use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; -class ChiDistTest extends TestCase +class ChiDistRightTailTest extends TestCase { protected function setUp(): void { @@ -26,6 +26,6 @@ class ChiDistTest extends TestCase public function providerCHIDIST() { - return require 'tests/data/Calculation/Statistical/CHIDIST.php'; + return require 'tests/data/Calculation/Statistical/CHIDISTRightTail.php'; } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvRightTailTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvRightTailTest.php new file mode 100644 index 00000000..75949f39 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvRightTailTest.php @@ -0,0 +1,37 @@ + [ + '#NUM!', + -8, 3, true, + ], + 'Degrees < 1' => [ + '#NUM!', + 8, 0, true, + ], +]; diff --git a/tests/data/Calculation/Statistical/CHIDIST.php b/tests/data/Calculation/Statistical/CHIDISTRightTail.php similarity index 100% rename from tests/data/Calculation/Statistical/CHIDIST.php rename to tests/data/Calculation/Statistical/CHIDISTRightTail.php diff --git a/tests/data/Calculation/Statistical/CHIINV.php b/tests/data/Calculation/Statistical/CHIINVRightTail.php similarity index 82% rename from tests/data/Calculation/Statistical/CHIINV.php rename to tests/data/Calculation/Statistical/CHIINVRightTail.php index f931a780..58b317e5 100644 --- a/tests/data/Calculation/Statistical/CHIINV.php +++ b/tests/data/Calculation/Statistical/CHIINVRightTail.php @@ -10,13 +10,21 @@ return [ 0.75, 10, ], [ - 18.30697345702, - 0.050001, 10, + 0.007716715545, + 0.93, 1, + ], + [ + 1.021651247532, + 0.6, 2, ], [ 0.45493642312, 0.5, 1, ], + [ + 4.351460191096, + 0.5, 5, + ], [ 0.101531044268, 0.75, 1, From e68978f1c7c33019a237ebbca43613a9671bd463 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 28 Mar 2021 19:12:45 +0200 Subject: [PATCH 71/89] Chi squared inverse left tailed (#1964) * Implementation of the CHISQ.INV() method for ChiSquared distribution left-tail --- CHANGELOG.md | 2 +- .../Calculation/Calculation.php | 2 +- .../Statistical/Distributions/ChiSquared.php | 131 ++++++++++++++++++ .../Statistical/ChiInvLeftTailTest.php | 37 +++++ .../Statistical/CHIINVLeftTail.php | 64 +++++++++ 5 files changed, 234 insertions(+), 2 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvLeftTailTest.php create mode 100644 tests/data/Calculation/Statistical/CHIINVLeftTail.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d7b123ef..eb3ab111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Added -- Implemented the CHITEST() and CHISQ.DIST() Statistical function. +- Implemented the CHITEST(), CHISQ.DIST() and CHISQ.INV() and equivalent Statistical functions, for both left- and right-tailed distributions. - Support for ActiveSheet and SelectedCells in the ODS Reader and Writer. [PR #1908](https://github.com/PHPOffice/PhpSpreadsheet/pull/1908) ### Changed diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index c4a88bc3..4dfcf9ab 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -508,7 +508,7 @@ class Calculation ], 'CHISQ.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Statistical\Distributions\ChiSquared::class, 'inverseLeftTail'], 'argumentCount' => '2', ], 'CHISQ.INV.RT' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 409e5883..df3451cd 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -11,6 +11,8 @@ class ChiSquared private const MAX_ITERATIONS = 256; + private const EPS = 2.22e-16; + /** * CHIDIST. * @@ -127,6 +129,35 @@ class ChiSquared return $newtonRaphson->execute($probability); } + /** + * CHIINV. + * + * Returns the inverse of the left-tailed probability of the chi-squared distribution. + * + * @param mixed (float) $probability Probability for the function + * @param mixed (int) $degrees degrees of freedom + * + * @return float|string + */ + public static function inverseLeftTail($probability, $degrees) + { + $probability = Functions::flattenSingleValue($probability); + $degrees = Functions::flattenSingleValue($degrees); + + try { + $probability = self::validateFloat($probability); + $degrees = self::validateInt($degrees); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($probability < 0.0 || $probability > 1.0 || $degrees < 1) { + return Functions::NAN(); + } + + return self::inverseLeftTailCalculation($probability, $degrees); + } + public static function test($actual, $expected) { $rows = count($actual); @@ -167,4 +198,104 @@ class ChiSquared return ($columns - 1) * ($rows - 1); } + + private static function inverseLeftTailCalculation($probability, $degrees) + { + // bracket the root + $min = 0; + $sd = sqrt(2.0 * $degrees); + $max = 2 * $sd; + $s = -1; + + while ($s * self::pchisq($max, $degrees) > $probability * $s) { + $min = $max; + $max += 2 * $sd; + } + + // Find root using bisection + $chi2 = 0.5 * ($min + $max); + + while (($max - $min) > self::EPS * $chi2) { + if ($s * self::pchisq($chi2, $degrees) > $probability * $s) { + $min = $chi2; + } else { + $max = $chi2; + } + $chi2 = 0.5 * ($min + $max); + } + + return $chi2; + } + + private static function pchisq($chi2, $degrees) + { + return self::gammp($degrees, 0.5 * $chi2); + } + + private static function gammp($n, $x) + { + if ($x < 0.5 * $n + 1) { + return self::gser($n, $x); + } + + return 1 - self::gcf($n, $x); + } + + // Return the incomplete gamma function P(n/2,x) evaluated by + // series representation. Algorithm from numerical recipe. + // Assume that n is a positive integer and x>0, won't check arguments. + // Relative error controlled by the eps parameter + private static function gser($n, $x) + { + $gln = Gamma::ln($n / 2); + $a = 0.5 * $n; + $ap = $a; + $sum = 1.0 / $a; + $del = $sum; + for ($i = 1; $i < 101; ++$i) { + ++$ap; + $del = $del * $x / $ap; + $sum += $del; + if ($del < $sum * self::EPS) { + break; + } + } + + return $sum * exp(-$x + $a * log($x) - $gln); + } + + // Return the incomplete gamma function Q(n/2,x) evaluated by + // its continued fraction representation. Algorithm from numerical recipe. + // Assume that n is a postive integer and x>0, won't check arguments. + // Relative error controlled by the eps parameter + private static function gcf($n, $x) + { + $gln = Gamma::ln($n / 2); + $a = 0.5 * $n; + $b = $x + 1 - $a; + $fpmin = 1.e-300; + $c = 1 / $fpmin; + $d = 1 / $b; + $h = $d; + for ($i = 1; $i < 101; ++$i) { + $an = -$i * ($i - $a); + $b += 2; + $d = $an * $d + $b; + if (abs($d) < $fpmin) { + $d = $fpmin; + } + $c = $b + $an / $c; + if (abs($c) < $fpmin) { + $c = $fpmin; + } + $d = 1 / $d; + $del = $d * $c; + $h = $h * $del; + if (abs($del - 1) < self::EPS) { + break; + } + } + + return $h * exp(-$x + $a * log($x) - $gln); + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvLeftTailTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvLeftTailTest.php new file mode 100644 index 00000000..962e20ad --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ChiInvLeftTailTest.php @@ -0,0 +1,37 @@ + [ + '#NUM!', + -0.1, 3, + ], + 'Probability > 1' => [ + '#NUM!', + 1.1, 3, + ], + 'Freedom > 1' => [ + '#NUM!', + 0.1, 0.5, + ], +]; From b87f93f8240f0e07761d1b336926af0bf748b2b3 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 28 Mar 2021 21:42:56 +0200 Subject: [PATCH 72/89] Update remaining references in the Financial Functions code to avoid calling deprecated methods (#1965) * Update remaining references in the Financial Functions code to bypass deprecated date methods and use the new date function methods directly --- src/PhpSpreadsheet/Calculation/Financial.php | 34 ++++++++++++------- .../Financial/Securities/AccruedInterest.php | 8 ++--- 2 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 1a67ef33..11bbf7a6 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -620,7 +620,7 @@ class Financial if (($price <= 0) || ($redemption <= 0)) { return Functions::NAN(); } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -810,7 +810,7 @@ class Financial if (($investment <= 0) || ($redemption <= 0)) { return Functions::NAN(); } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -1451,7 +1451,7 @@ class Financial if (($investment <= 0) || ($discount <= 0)) { return Functions::NAN(); } - $daysBetweenSettlementAndMaturity = DateTime::YEARFRAC($settlement, $maturity, $basis); + $daysBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, $basis); if (!is_numeric($daysBetweenSettlementAndMaturity)) { // return date error return $daysBetweenSettlementAndMaturity; @@ -1639,9 +1639,10 @@ class Financial $datesCount = count($dates); for ($i = 0; $i < $datesCount; ++$i) { - $dates[$i] = DateTime::getDateValue($dates[$i]); - if (!is_numeric($dates[$i])) { - return Functions::VALUE(); + try { + $dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]); + } catch (Exception $e) { + return $e->getMessage(); } } @@ -1766,7 +1767,7 @@ class Financial if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) { return Functions::NAN(); } - $date0 = DateTime::getDateValue($dates[0]); + $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); if (is_string($date0)) { return Functions::VALUE(); } @@ -1780,7 +1781,12 @@ class Financial $values = Functions::flattenArray($values); $dates = Functions::flattenArray($dates); $valCount = count($values); - $date0 = DateTime::getDateValue($dates[0]); + + try { + $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); + } catch (Exception $e) { + return $e->getMessage(); + } $rslt = self::validateXnpv($rate, $values, $dates); if ($rslt) { return $rslt; @@ -1790,14 +1796,16 @@ class Financial if (!is_numeric($values[$i])) { return Functions::VALUE(); } - $datei = DateTime::getDateValue($dates[$i]); - if (is_string($datei)) { - return Functions::VALUE(); + + try { + $datei = DateTimeExcel\Helpers::getDateValue($dates[$i]); + } catch (Exception $e) { + return $e->getMessage(); } if ($date0 > $datei) { - $dif = $ordered ? Functions::NAN() : -DateTime::DATEDIF($datei, $date0, 'd'); + $dif = $ordered ? Functions::NAN() : -DateTimeExcel\DateDif::funcDateDif($datei, $date0, 'd'); } else { - $dif = DateTime::DATEDIF($date0, $datei, 'd'); + $dif = DateTimeExcel\DateDif::funcDateDif($date0, $datei, 'd'); } if (!is_numeric($dif)) { return $dif; diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php index f81ea13c..726b125a 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\Securities; -use PhpOffice\PhpSpreadsheet\Calculation\DateTime; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\YearFrac; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -76,12 +76,12 @@ class AccruedInterest return $e->getMessage(); } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = YearFrac::funcYearFrac($issue, $settlement, $basis); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; } - $daysBetweenFirstInterestAndSettlement = DateTime::YEARFRAC($firstinterest, $settlement, $basis); + $daysBetweenFirstInterestAndSettlement = YearFrac::funcYearFrac($firstinterest, $settlement, $basis); if (!is_numeric($daysBetweenFirstInterestAndSettlement)) { // return date error return $daysBetweenFirstInterestAndSettlement; @@ -132,7 +132,7 @@ class AccruedInterest return $e->getMessage(); } - $daysBetweenIssueAndSettlement = DateTime::YEARFRAC($issue, $settlement, $basis); + $daysBetweenIssueAndSettlement = YearFrac::funcYearFrac($issue, $settlement, $basis); if (!is_numeric($daysBetweenIssueAndSettlement)) { // return date error return $daysBetweenIssueAndSettlement; From 1c92b7611ab1bb3b6e43c6380830361c7e723d16 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Mon, 29 Mar 2021 12:59:46 +0200 Subject: [PATCH 73/89] Extract Percentile-type functions from Statistics (#1966) * Extract Percentile-type functions from Statistics (e.g. PERCENTILE(), PERCENTRANK(), QUARTILE(), and RANK()) * Unit test for PERCENTILE() with an empty (of numbers) dataset --- .../Calculation/Calculation.php | 14 +- .../Calculation/Statistical.php | 132 +++-------- .../Statistical/BaseValidations.php | 27 +++ .../Calculation/Statistical/Confidence.php | 25 ++- .../Calculation/Statistical/Percentiles.php | 207 ++++++++++++++++++ .../Calculation/Statistical/Permutations.php | 40 ++-- .../Calculation/Statistical/Trends.php | 6 +- .../Calculation/Statistical/PERCENTILE.php | 24 ++ .../Calculation/Statistical/PERCENTRANK.php | 7 +- .../Calculation/Statistical/PERMUTATIONA.php | 8 + .../data/Calculation/Statistical/QUARTILE.php | 4 + tests/data/Calculation/Statistical/RANK.php | 41 ++-- 12 files changed, 378 insertions(+), 157 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/BaseValidations.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 4dfcf9ab..4f95c5b4 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1903,7 +1903,7 @@ class Calculation ], 'PERCENTILE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'PERCENTILE'], + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'], 'argumentCount' => '2', ], 'PERCENTILE.EXC' => [ @@ -1913,12 +1913,12 @@ class Calculation ], 'PERCENTILE.INC' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'PERCENTILE'], + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTILE'], 'argumentCount' => '2', ], 'PERCENTRANK' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'PERCENTRANK'], + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'], 'argumentCount' => '2,3', ], 'PERCENTRANK.EXC' => [ @@ -1928,7 +1928,7 @@ class Calculation ], 'PERCENTRANK.INC' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'PERCENTRANK'], + 'functionCall' => [Statistical\Percentiles::class, 'PERCENTRANK'], 'argumentCount' => '2,3', ], 'PERMUT' => [ @@ -2018,7 +2018,7 @@ class Calculation ], 'QUARTILE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'QUARTILE'], + 'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'], 'argumentCount' => '2', ], 'QUARTILE.EXC' => [ @@ -2028,7 +2028,7 @@ class Calculation ], 'QUARTILE.INC' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'QUARTILE'], + 'functionCall' => [Statistical\Percentiles::class, 'QUARTILE'], 'argumentCount' => '2', ], 'QUOTIENT' => [ @@ -2058,7 +2058,7 @@ class Calculation ], 'RANK' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'RANK'], + 'functionCall' => [Statistical\Percentiles::class, 'RANK'], 'argumentCount' => '2,3', ], 'RANK.AVG' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 01caba14..ce80fa79 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -1665,45 +1665,18 @@ class Statistical * Excel Function: * PERCENTILE(value1[,value2[, ...]],entry) * + * @Deprecated 1.18.0 + * + * @see Statistical\Percentiles::PERCENTILE() + * Use the PERCENTILE() method in the Statistical\Percentiles class instead + * * @param mixed $args Data values * * @return float|string The result, or a string containing an error */ public static function PERCENTILE(...$args) { - $aArgs = Functions::flattenArray($args); - - // Calculate - $entry = array_pop($aArgs); - - if ((is_numeric($entry)) && (!is_string($entry))) { - if (($entry < 0) || ($entry > 1)) { - return Functions::NAN(); - } - $mArgs = []; - foreach ($aArgs as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $mArgs[] = $arg; - } - } - $mValueCount = count($mArgs); - if ($mValueCount > 0) { - sort($mArgs); - $count = Counts::COUNT($mArgs); - $index = $entry * ($count - 1); - $iBase = floor($index); - if ($index == $iBase) { - return $mArgs[$index]; - } - $iNext = $iBase + 1; - $iProportion = $index - $iBase; - - return $mArgs[$iBase] + (($mArgs[$iNext] - $mArgs[$iBase]) * $iProportion); - } - } - - return Functions::VALUE(); + return Statistical\Percentiles::PERCENTILE(...$args); } /** @@ -1714,6 +1687,11 @@ class Statistical * rather than floored (as MS Excel), so value 3 for a value set of 1, 2, 3, 4 will return * 0.667 rather than 0.666 * + * @Deprecated 1.18.0 + * + * @see Statistical\Percentiles::PERCENTRANK() + * Use the PERCENTRANK() method in the Statistical\Percentiles class instead + * * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers * @param mixed (int) $value the number whose rank you want to find * @param mixed (int) $significance the number of significant digits for the returned percentage value @@ -1722,38 +1700,7 @@ class Statistical */ public static function PERCENTRANK($valueSet, $value, $significance = 3) { - $valueSet = Functions::flattenArray($valueSet); - $value = Functions::flattenSingleValue($value); - $significance = ($significance === null) ? 3 : (int) Functions::flattenSingleValue($significance); - - foreach ($valueSet as $key => $valueEntry) { - if (!is_numeric($valueEntry)) { - unset($valueSet[$key]); - } - } - sort($valueSet, SORT_NUMERIC); - $valueCount = count($valueSet); - if ($valueCount == 0) { - return Functions::NAN(); - } - - $valueAdjustor = $valueCount - 1; - if (($value < $valueSet[0]) || ($value > $valueSet[$valueAdjustor])) { - return Functions::NA(); - } - - $pos = array_search($value, $valueSet); - if ($pos === false) { - $pos = 0; - $testValue = $valueSet[0]; - while ($testValue < $value) { - $testValue = $valueSet[++$pos]; - } - --$pos; - $pos += (($value - $valueSet[$pos]) / ($testValue - $valueSet[$pos])); - } - - return round($pos / $valueAdjustor, $significance); + return Statistical\Percentiles::PERCENTRANK($valueSet, $value, $significance); } /** @@ -1811,27 +1758,18 @@ class Statistical * Excel Function: * QUARTILE(value1[,value2[, ...]],entry) * + * @Deprecated 1.18.0 + * + * @see Statistical\Percentiles::QUARTILE() + * Use the QUARTILE() method in the Statistical\Percentiles class instead + * * @param mixed $args Data values * * @return float|string The result, or a string containing an error */ public static function QUARTILE(...$args) { - $aArgs = Functions::flattenArray($args); - $entry = array_pop($aArgs); - - // Calculate - if ((is_numeric($entry)) && (!is_string($entry))) { - $entry = floor($entry); - $entry /= 4; - if (($entry < 0) || ($entry > 1)) { - return Functions::NAN(); - } - - return self::PERCENTILE($aArgs, $entry); - } - - return Functions::VALUE(); + return Statistical\Percentiles::QUARTILE(...$args); } /** @@ -1839,36 +1777,20 @@ class Statistical * * Returns the rank of a number in a list of numbers. * - * @param int $value the number whose rank you want to find - * @param float[] $valueSet An array of, or a reference to, a list of numbers - * @param int $order Order to sort the values in the value set + * @Deprecated 1.18.0 + * + * @see Statistical\Percentiles::RANK() + * Use the RANK() method in the Statistical\Percentiles class instead + * + * @param mixed (float) $value the number whose rank you want to find + * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers + * @param mixed (int) $order Order to sort the values in the value set * * @return float|string The result, or a string containing an error */ public static function RANK($value, $valueSet, $order = 0) { - $value = Functions::flattenSingleValue($value); - $valueSet = Functions::flattenArray($valueSet); - $order = ($order === null) ? 0 : (int) Functions::flattenSingleValue($order); - - foreach ($valueSet as $key => $valueEntry) { - if (!is_numeric($valueEntry)) { - unset($valueSet[$key]); - } - } - - if ($order == 0) { - sort($valueSet, SORT_NUMERIC); - } else { - rsort($valueSet, SORT_NUMERIC); - } - - $pos = array_search($value, $valueSet); - if ($pos === false) { - return Functions::NA(); - } - - return ++$pos; + return Statistical\Percentiles::RANK($value, $valueSet, $order); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Statistical/BaseValidations.php new file mode 100644 index 00000000..1dbe4212 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/BaseValidations.php @@ -0,0 +1,27 @@ += 1)) { - return Functions::NAN(); - } - if (($stdDev <= 0) || ($size < 1)) { - return Functions::NAN(); - } - - return Statistical::NORMSINV(1 - $alpha / 2) * $stdDev / sqrt($size); + try { + $alpha = self::validateFloat($alpha); + $stdDev = self::validateFloat($stdDev); + $size = self::validateInt($size); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if (($alpha <= 0) || ($alpha >= 1) || ($stdDev <= 0) || ($size < 1)) { + return Functions::NAN(); + } + + return Statistical::NORMSINV(1 - $alpha / 2) * $stdDev / sqrt($size); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php b/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php new file mode 100644 index 00000000..0001b7bf --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php @@ -0,0 +1,207 @@ +getMessage(); + } + + if (($entry < 0) || ($entry > 1)) { + return Functions::NAN(); + } + + $mArgs = self::percentileFilterValues($aArgs); + $mValueCount = count($mArgs); + if ($mValueCount > 0) { + sort($mArgs); + $count = Counts::COUNT($mArgs); + $index = $entry * ($count - 1); + $iBase = floor($index); + if ($index == $iBase) { + return $mArgs[$index]; + } + $iNext = $iBase + 1; + $iProportion = $index - $iBase; + + return $mArgs[$iBase] + (($mArgs[$iNext] - $mArgs[$iBase]) * $iProportion); + } + + return Functions::NAN(); + } + + /** + * PERCENTRANK. + * + * Returns the rank of a value in a data set as a percentage of the data set. + * Note that the returned rank is simply rounded to the appropriate significant digits, + * rather than floored (as MS Excel), so value 3 for a value set of 1, 2, 3, 4 will return + * 0.667 rather than 0.666 + * + * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers + * @param mixed (int) $value the number whose rank you want to find + * @param mixed (int) $significance the number of significant digits for the returned percentage value + * + * @return float|string (string if result is an error) + */ + public static function PERCENTRANK($valueSet, $value, $significance = 3) + { + $valueSet = Functions::flattenArray($valueSet); + $value = Functions::flattenSingleValue($value); + $significance = ($significance === null) ? 3 : Functions::flattenSingleValue($significance); + + try { + $value = self::validateFloat($value); + $significance = self::validateInt($significance); + } catch (Exception $e) { + return $e->getMessage(); + } + + $valueSet = self::rankFilterValues($valueSet); + $valueCount = count($valueSet); + if ($valueCount == 0) { + return Functions::NA(); + } + sort($valueSet, SORT_NUMERIC); + + $valueAdjustor = $valueCount - 1; + if (($value < $valueSet[0]) || ($value > $valueSet[$valueAdjustor])) { + return Functions::NA(); + } + + $pos = array_search($value, $valueSet); + if ($pos === false) { + $pos = 0; + $testValue = $valueSet[0]; + while ($testValue < $value) { + $testValue = $valueSet[++$pos]; + } + --$pos; + $pos += (($value - $valueSet[$pos]) / ($testValue - $valueSet[$pos])); + } + + return round($pos / $valueAdjustor, $significance); + } + + /** + * QUARTILE. + * + * Returns the quartile of a data set. + * + * Excel Function: + * QUARTILE(value1[,value2[, ...]],entry) + * + * @param mixed $args Data values + * + * @return float|string The result, or a string containing an error + */ + public static function QUARTILE(...$args) + { + $aArgs = Functions::flattenArray($args); + $entry = array_pop($aArgs); + + try { + $entry = self::validateFloat($entry); + } catch (Exception $e) { + return $e->getMessage(); + } + + $entry = floor($entry); + $entry /= 4; + if (($entry < 0) || ($entry > 1)) { + return Functions::NAN(); + } + + return self::PERCENTILE($aArgs, $entry); + } + + /** + * RANK. + * + * Returns the rank of a number in a list of numbers. + * + * @param mixed (float) $value the number whose rank you want to find + * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers + * @param mixed (int) $order Order to sort the values in the value set + * + * @return float|string The result, or a string containing an error + */ + public static function RANK($value, $valueSet, $order = self::RANK_SORT_DESCENDING) + { + $value = Functions::flattenSingleValue($value); + $valueSet = Functions::flattenArray($valueSet); + $order = ($order === null) ? self::RANK_SORT_DESCENDING : Functions::flattenSingleValue($order); + + try { + $value = self::validateFloat($value); + $order = self::validateInt($order); + } catch (Exception $e) { + return $e->getMessage(); + } + + $valueSet = self::rankFilterValues($valueSet); + if ($order === self::RANK_SORT_DESCENDING) { + rsort($valueSet, SORT_NUMERIC); + } else { + sort($valueSet, SORT_NUMERIC); + } + + $pos = array_search($value, $valueSet); + if ($pos === false) { + return Functions::NA(); + } + + return ++$pos; + } + + protected static function percentileFilterValues(array $dataSet) + { + return array_filter( + $dataSet, + function ($value): bool { + return is_numeric($value) && !is_string($value); + } + ); + } + + protected static function rankFilterValues(array $dataSet) + { + return array_filter( + $dataSet, + function ($value): bool { + return is_numeric($value); + } + ); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php index 343a056c..84cdfea1 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -2,11 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; class Permutations { + use BaseValidations; + /** * PERMUT. * @@ -26,16 +29,18 @@ class Permutations $numObjs = Functions::flattenSingleValue($numObjs); $numInSet = Functions::flattenSingleValue($numInSet); - if ((is_numeric($numObjs)) && (is_numeric($numInSet))) { - $numInSet = floor($numInSet); - if ($numObjs < $numInSet) { - return Functions::NAN(); - } - - return round(MathTrig\Fact::funcFact($numObjs) / MathTrig\Fact::funcFact($numObjs - $numInSet)); + try { + $numObjs = self::validateInt($numObjs); + $numInSet = self::validateInt($numInSet); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if ($numObjs < $numInSet) { + return Functions::NAN(); + } + + return round(MathTrig\Fact::funcFact($numObjs) / MathTrig\Fact::funcFact($numObjs - $numInSet)); } /** @@ -54,16 +59,17 @@ class Permutations $numObjs = Functions::flattenSingleValue($numObjs); $numInSet = Functions::flattenSingleValue($numInSet); - if ((is_numeric($numObjs)) && (is_numeric($numInSet))) { - $numObjs = floor($numObjs); - $numInSet = floor($numInSet); - if ($numObjs < 0 || $numInSet < 0) { - return Functions::NAN(); - } - - return $numObjs ** $numInSet; + try { + $numObjs = self::validateInt($numObjs); + $numInSet = self::validateInt($numInSet); + } catch (Exception $e) { + return $e->getMessage(); } - return Functions::VALUE(); + if ($numObjs < 0 || $numInSet < 0) { + return Functions::NAN(); + } + + return $numObjs ** $numInSet; } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index b1dfbaef..8c88c54c 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -8,6 +8,8 @@ use PhpOffice\PhpSpreadsheet\Shared\Trend\Trend; class Trends { + use BaseValidations; + private static function filterTrendValues(array &$array1, array &$array2): void { foreach ($array1 as $key => $value) { @@ -116,11 +118,9 @@ class Trends public static function FORECAST($xValue, $yValues, $xValues) { $xValue = Functions::flattenSingleValue($xValue); - if (!is_numeric($xValue)) { - return Functions::VALUE(); - } try { + $xValue = self::validateFloat($xValue); self::checkTrendArrays($yValues, $xValues); self::validateTrendArrays($yValues, $xValues); } catch (Exception $e) { diff --git a/tests/data/Calculation/Statistical/PERCENTILE.php b/tests/data/Calculation/Statistical/PERCENTILE.php index 121e49c0..cf08ce88 100644 --- a/tests/data/Calculation/Statistical/PERCENTILE.php +++ b/tests/data/Calculation/Statistical/PERCENTILE.php @@ -25,10 +25,34 @@ return [ 48.4, [10.5, 7.2, 200, 5.4, 8.1, 0.8], ], + [ + 2, + [2, 1, 6, 4, 3, 5, 0.2], + ], + [ + 4, + [2, 1, 6, 4, 3, 5, 0.6], + ], + [ + 3.5, + [2, 1, 6, 4, 3, 5, 0.5], + ], + [ + 5.75, + [2, 1, 6, 4, 3, 5, 0.95], + ], [ '#NUM!', [1, 2, 3, 4, -0.3], ], + [ + '#NUM!', + [1, 2, 3, 4, 1.5], + ], + [ + '#NUM!', + ['A', 'B', 0.5], + ], [ '#VALUE!', [1, 2, 3, 4, 'NaN'], diff --git a/tests/data/Calculation/Statistical/PERCENTRANK.php b/tests/data/Calculation/Statistical/PERCENTRANK.php index 3ab019ac..3787a7ac 100644 --- a/tests/data/Calculation/Statistical/PERCENTRANK.php +++ b/tests/data/Calculation/Statistical/PERCENTRANK.php @@ -56,10 +56,15 @@ return [ 2, ], [ - '#NUM!', + '#VALUE!', ['A', 'B', 'C', 'D'], 'E', ], + [ + '#N/A', + ['A', 'B', 'C', 'D'], + 3, + ], [ '#N/A', [1, 2, 3, 4], diff --git a/tests/data/Calculation/Statistical/PERMUTATIONA.php b/tests/data/Calculation/Statistical/PERMUTATIONA.php index 6bc118b3..701f5eac 100644 --- a/tests/data/Calculation/Statistical/PERMUTATIONA.php +++ b/tests/data/Calculation/Statistical/PERMUTATIONA.php @@ -21,6 +21,14 @@ return [ '#NUM!', -1, 2, ], + [ + '#NUM!', + 1, -2, + ], + [ + '#VALUE!', + 'NaN', 31, + ], [ '#VALUE!', 49, 'NaN', diff --git a/tests/data/Calculation/Statistical/QUARTILE.php b/tests/data/Calculation/Statistical/QUARTILE.php index 80b2bf09..26a7902f 100644 --- a/tests/data/Calculation/Statistical/QUARTILE.php +++ b/tests/data/Calculation/Statistical/QUARTILE.php @@ -37,6 +37,10 @@ return [ 9.25, [7, 8, 9, 10, 3], ], + [ + '#NUM!', + [7, 8, 9, 10, -1], + ], [ '#NUM!', [7, 8, 9, 10, 5], diff --git a/tests/data/Calculation/Statistical/RANK.php b/tests/data/Calculation/Statistical/RANK.php index 0640bb43..6cb60e24 100644 --- a/tests/data/Calculation/Statistical/RANK.php +++ b/tests/data/Calculation/Statistical/RANK.php @@ -1,38 +1,53 @@ Date: Tue, 30 Mar 2021 10:11:19 +0900 Subject: [PATCH 74/89] Document release process --- CONTRIBUTING.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aed13fe2..f5953533 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,3 +9,12 @@ If you would like to contribute, here are some notes and guidelines: - All code changes must be validated by `composer check` - [Helpful article about forking](https://help.github.com/articles/fork-a-repo/ "Forking a GitHub repository") - [Helpful article about pull requests](https://help.github.com/articles/using-pull-requests/ "Pull Requests") + +## How to release + +1. Complete CHANGELOG.md and commit +2. Create an annotated tag + 1. `git tag -a 1.2.3` + 2. Tag subject must be the version number, eg: `1.2.3` + 3. Tag body must be a copy-paste of the changelog entries +3. Push tag with `git push --tags`, GitHub Actions will create a GitHub release automatically From 029f345987b4f1086fe4134d6dd5303f0e50e691 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 30 Mar 2021 22:49:10 +0200 Subject: [PATCH 75/89] Extract Binomial Distribution functions from Statistical (#1974) * Extract Binomial Distribution functions from Statistical Replace the old MS algorithm for CRITBINOM() (which has now been replaced with te BINOM.INV() function) with a brute force approach - I'll look to refine it later. The MS algorithm is no longer documented, and the implementation produced erroneous results anyway * Exract the NEGBINOMDIST() function as well; still need to add a cumulative flag to support the additional argument for the newer NEGBINOM.DIST() function * Rationalise validation of probability arguments --- .../Calculation/Calculation.php | 12 +- .../Calculation/Financial/CashFlow/Single.php | 7 + .../Calculation/Statistical.php | 170 ++------------- .../Distributions/BaseValidations.php | 11 + .../Statistical/Distributions/Beta.php | 4 +- .../Statistical/Distributions/Binomial.php | 200 ++++++++++++++++++ .../Statistical/Distributions/ChiSquared.php | 8 +- .../Statistical/Distributions/Gamma.php | 4 +- .../Distributions/NewtonRaphson.php | 2 +- .../Statistical/Distributions/StudentT.php | 4 +- .../Statistical/BinomDistRangeTest.php | 31 +++ .../Functions/Statistical/BinomInvTest.php | 31 +++ .../Calculation/Statistical/BINOMDIST.php | 22 +- .../Statistical/BINOMDISTRANGE.php | 72 +++++++ .../data/Calculation/Statistical/BINOMINV.php | 60 ++++++ .../Calculation/Statistical/CRITBINOM.php | 24 --- .../Calculation/Statistical/NEGBINOMDIST.php | 16 ++ 17 files changed, 484 insertions(+), 194 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/BinomDistRangeTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/BinomInvTest.php create mode 100644 tests/data/Calculation/Statistical/BINOMDISTRANGE.php create mode 100644 tests/data/Calculation/Statistical/BINOMINV.php delete mode 100644 tests/data/Calculation/Statistical/CRITBINOM.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 4f95c5b4..2026ce62 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -418,22 +418,22 @@ class Calculation ], 'BINOMDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'BINOMDIST'], + 'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'], 'argumentCount' => '4', ], 'BINOM.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'BINOMDIST'], + 'functionCall' => [Statistical\Distributions\Binomial::class, 'distribution'], 'argumentCount' => '4', ], 'BINOM.DIST.RANGE' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Statistical\Distributions\Binomial::class, 'range'], 'argumentCount' => '3,4', ], 'BINOM.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'], 'argumentCount' => '3', ], 'BITAND' => [ @@ -695,7 +695,7 @@ class Calculation ], 'CRITBINOM' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'CRITBINOM'], + 'functionCall' => [Statistical\Distributions\Binomial::class, 'inverse'], 'argumentCount' => '3', ], 'CSC' => [ @@ -1751,7 +1751,7 @@ class Calculation ], 'NEGBINOMDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NEGBINOMDIST'], + 'functionCall' => [Statistical\Distributions\Binomial::class, 'negative'], 'argumentCount' => '3', ], 'NEGBINOM.DIST' => [ diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php new file mode 100644 index 00000000..3f1c8bc6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php @@ -0,0 +1,7 @@ + $trials)) { - return Functions::NAN(); - } - if (($probability < 0) || ($probability > 1)) { - return Functions::NAN(); - } - if ((is_numeric($cumulative)) || (is_bool($cumulative))) { - if ($cumulative) { - $summer = 0; - for ($i = 0; $i <= $value; ++$i) { - $summer += MathTrig::COMBIN($trials, $i) * $probability ** $i * (1 - $probability) ** ($trials - $i); - } - - return $summer; - } - - return MathTrig::COMBIN($trials, $value) * $probability ** $value * (1 - $probability) ** ($trials - $value); - } - } - - return Functions::VALUE(); + return Statistical\Distributions\Binomial::distribution($value, $trials, $probability, $cumulative); } /** @@ -510,6 +488,11 @@ class Statistical * * See https://support.microsoft.com/en-us/help/828117/ for details of the algorithm used * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Binomial::inverse() + * Use the inverse() method in the Statistical\Distributions\Binomial class instead + * * @param float $trials number of Bernoulli trials * @param float $probability probability of a success on each trial * @param float $alpha criterion value @@ -523,110 +506,7 @@ class Statistical */ public static function CRITBINOM($trials, $probability, $alpha) { - $trials = floor(Functions::flattenSingleValue($trials)); - $probability = Functions::flattenSingleValue($probability); - $alpha = Functions::flattenSingleValue($alpha); - - if ((is_numeric($trials)) && (is_numeric($probability)) && (is_numeric($alpha))) { - $trials = (int) $trials; - if ($trials < 0) { - return Functions::NAN(); - } elseif (($probability < 0.0) || ($probability > 1.0)) { - return Functions::NAN(); - } elseif (($alpha < 0.0) || ($alpha > 1.0)) { - return Functions::NAN(); - } - - if ($alpha <= 0.5) { - $t = sqrt(log(1 / ($alpha * $alpha))); - $trialsApprox = 0 - ($t + (2.515517 + 0.802853 * $t + 0.010328 * $t * $t) / (1 + 1.432788 * $t + 0.189269 * $t * $t + 0.001308 * $t * $t * $t)); - } else { - $t = sqrt(log(1 / (1 - $alpha) ** 2)); - $trialsApprox = $t - (2.515517 + 0.802853 * $t + 0.010328 * $t * $t) / (1 + 1.432788 * $t + 0.189269 * $t * $t + 0.001308 * $t * $t * $t); - } - - $Guess = floor($trials * $probability + $trialsApprox * sqrt($trials * $probability * (1 - $probability))); - if ($Guess < 0) { - $Guess = 0; - } elseif ($Guess > $trials) { - $Guess = $trials; - } - - $TotalUnscaledProbability = $UnscaledPGuess = $UnscaledCumPGuess = 0.0; - $EssentiallyZero = 10e-12; - - $m = floor($trials * $probability); - ++$TotalUnscaledProbability; - if ($m == $Guess) { - ++$UnscaledPGuess; - } - if ($m <= $Guess) { - ++$UnscaledCumPGuess; - } - - $PreviousValue = 1; - $Done = false; - $k = $m + 1; - while ((!$Done) && ($k <= $trials)) { - $CurrentValue = $PreviousValue * ($trials - $k + 1) * $probability / ($k * (1 - $probability)); - $TotalUnscaledProbability += $CurrentValue; - if ($k == $Guess) { - $UnscaledPGuess += $CurrentValue; - } - if ($k <= $Guess) { - $UnscaledCumPGuess += $CurrentValue; - } - if ($CurrentValue <= $EssentiallyZero) { - $Done = true; - } - $PreviousValue = $CurrentValue; - ++$k; - } - - $PreviousValue = 1; - $Done = false; - $k = $m - 1; - while ((!$Done) && ($k >= 0)) { - $CurrentValue = $PreviousValue * $k + 1 * (1 - $probability) / (($trials - $k) * $probability); - $TotalUnscaledProbability += $CurrentValue; - if ($k == $Guess) { - $UnscaledPGuess += $CurrentValue; - } - if ($k <= $Guess) { - $UnscaledCumPGuess += $CurrentValue; - } - if ($CurrentValue <= $EssentiallyZero) { - $Done = true; - } - $PreviousValue = $CurrentValue; - --$k; - } - - $PGuess = $UnscaledPGuess / $TotalUnscaledProbability; - $CumPGuess = $UnscaledCumPGuess / $TotalUnscaledProbability; - - $CumPGuessMinus1 = $CumPGuess - 1; - - while (true) { - if (($CumPGuessMinus1 < $alpha) && ($CumPGuess >= $alpha)) { - return $Guess; - } elseif (($CumPGuessMinus1 < $alpha) && ($CumPGuess < $alpha)) { - $PGuessPlus1 = $PGuess * ($trials - $Guess) * $probability / $Guess / (1 - $probability); - $CumPGuessMinus1 = $CumPGuess; - $CumPGuess = $CumPGuess + $PGuessPlus1; - $PGuess = $PGuessPlus1; - ++$Guess; - } elseif (($CumPGuessMinus1 >= $alpha) && ($CumPGuess >= $alpha)) { - $PGuessMinus1 = $PGuess * $Guess * (1 - $probability) / ($trials - $Guess + 1) / $probability; - $CumPGuess = $CumPGuessMinus1; - $CumPGuessMinus1 = $CumPGuessMinus1 - $PGuess; - $PGuess = $PGuessMinus1; - --$Guess; - } - } - } - - return Functions::VALUE(); + return Statistical\Distributions\Binomial::inverse($trials, $probability, $alpha); } /** @@ -1502,6 +1382,11 @@ class Statistical * distribution, except that the number of successes is fixed, and the number of trials is * variable. Like the binomial, trials are assumed to be independent. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Binomial::negative::mode() + * Use the negative() method in the Statistical\Distributions\Binomial class instead + * * @param mixed (float) $failures Number of Failures * @param mixed (float) $successes Threshold number of Successes * @param mixed (float) $probability Probability of success on each trial @@ -1510,26 +1395,7 @@ class Statistical */ public static function NEGBINOMDIST($failures, $successes, $probability) { - $failures = floor(Functions::flattenSingleValue($failures)); - $successes = floor(Functions::flattenSingleValue($successes)); - $probability = Functions::flattenSingleValue($probability); - - if ((is_numeric($failures)) && (is_numeric($successes)) && (is_numeric($probability))) { - if (($failures < 0) || ($successes < 1)) { - return Functions::NAN(); - } elseif (($probability < 0) || ($probability > 1)) { - return Functions::NAN(); - } - if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { - if (($failures + $successes - 1) <= 0) { - return Functions::NAN(); - } - } - - return (MathTrig::COMBIN($failures + $successes - 1, $successes - 1)) * ($probability ** $successes) * ((1 - $probability) ** $failures); - } - - return Functions::VALUE(); + return Statistical\Distributions\Binomial::negative($failures, $successes, $probability); } /** diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php index a8ab3e89..a2e0b042 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/BaseValidations.php @@ -33,4 +33,15 @@ trait BaseValidations return (bool) $value; } + + protected static function validateProbability($probability) + { + $probability = self::validateFloat($probability); + + if ($probability < 0.0 || $probability > 1.0) { + throw new Exception(Functions::NAN()); + } + + return $probability; + } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php index c18973ce..30b8d02a 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php @@ -83,7 +83,7 @@ class Beta $rMax = Functions::flattenSingleValue($rMax); try { - $probability = self::validateFloat($probability); + $probability = self::validateProbability($probability); $alpha = self::validateFloat($alpha); $beta = self::validateFloat($beta); $rMax = self::validateFloat($rMax); @@ -97,7 +97,7 @@ class Beta $rMin = $rMax; $rMax = $tmp; } - if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0) || ($probability > 1)) { + if (($alpha <= 0) || ($beta <= 0) || ($rMin == $rMax) || ($probability <= 0.0)) { return Functions::NAN(); } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php new file mode 100644 index 00000000..2ab1fe67 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php @@ -0,0 +1,200 @@ +getMessage(); + } + + if (($value < 0) || ($value > $trials)) { + return Functions::NAN(); + } + + if ($cumulative) { + return self::calculateCumulativeBinomial($value, $trials, $probability); + } + + return MathTrig::COMBIN($trials, $value) * $probability ** $value * (1 - $probability) ** ($trials - $value); + } + + /** + * BINOM.DIST.RANGE. + * + * Returns returns the Binomial Distribution probability for the number of successes from a specified number + * of trials falling into a specified range. + * + * @param mixed (int) $trials Number of trials + * @param mixed (float) $probability Probability of success on each trial + * @param mixed (int) $successes The number of successes in trials + * @param mixed (int) $limit Upper limit for successes in trials + * + * @return float|string + */ + 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); + + try { + $trials = self::validateInt($trials); + $probability = self::validateProbability($probability); + $successes = self::validateInt($successes); + $limit = self::validateInt($limit); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($successes < 0) || ($successes > $trials)) { + return Functions::NAN(); + } + if (($limit < 0) || ($limit > $trials) || $limit < $successes) { + return Functions::NAN(); + } + + $summer = 0; + for ($i = $successes; $i <= $limit; ++$i) { + $summer += MathTrig::COMBIN($trials, $i) * $probability ** $i * (1 - $probability) ** ($trials - $i); + } + + return $summer; + } + + /** + * NEGBINOMDIST. + * + * Returns the negative binomial distribution. NEGBINOMDIST returns the probability that + * there will be number_f failures before the number_s-th success, when the constant + * probability of a success is probability_s. This function is similar to the binomial + * distribution, except that the number of successes is fixed, and the number of trials is + * variable. Like the binomial, trials are assumed to be independent. + * + * @param mixed (float) $failures Number of Failures + * @param mixed (float) $successes Threshold number of Successes + * @param mixed (float) $probability Probability of success on each trial + * + * @return float|string The result, or a string containing an error + * + * 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); + + try { + $failures = self::validateInt($failures); + $successes = self::validateInt($successes); + $probability = self::validateProbability($probability); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($failures < 0) || ($successes < 1)) { + return Functions::NAN(); + } + if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) { + if (($failures + $successes - 1) <= 0) { + return Functions::NAN(); + } + } + + return (MathTrig::COMBIN($failures + $successes - 1, $successes - 1)) * + ($probability ** $successes) * ((1 - $probability) ** $failures); + } + + /** + * CRITBINOM. + * + * Returns the smallest value for which the cumulative binomial distribution is greater + * than or equal to a criterion value + * + * @param float $trials number of Bernoulli trials + * @param float $probability probability of a success on each trial + * @param float $alpha criterion value + * + * @return int|string + */ + public static function inverse($trials, $probability, $alpha) + { + $trials = Functions::flattenSingleValue($trials); + $probability = Functions::flattenSingleValue($probability); + $alpha = Functions::flattenSingleValue($alpha); + + try { + $trials = self::validateInt($trials); + $probability = self::validateProbability($probability); + $alpha = self::validateFloat($alpha); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($trials < 0) { + return Functions::NAN(); + } elseif (($alpha < 0.0) || ($alpha > 1.0)) { + return Functions::NAN(); + } + + $successes = 0; + while (true && $successes <= $trials) { + $result = self::calculateCumulativeBinomial($successes, $trials, $probability); + if ($result >= $alpha) { + break; + } + ++$successes; + } + + return $successes; + } + + /** + * @return float|int + */ + private static function calculateCumulativeBinomial(int $value, int $trials, float $probability) + { + $summer = 0; + for ($i = 0; $i <= $value; ++$i) { + $summer += MathTrig::COMBIN($trials, $i) * $probability ** $i * (1 - $probability) ** ($trials - $i); + } + + return $summer; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index df3451cd..3ebe1dc5 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -109,13 +109,13 @@ class ChiSquared $degrees = Functions::flattenSingleValue($degrees); try { - $probability = self::validateFloat($probability); + $probability = self::validateProbability($probability); $degrees = self::validateInt($degrees); } catch (Exception $e) { return $e->getMessage(); } - if ($probability < 0.0 || $probability > 1.0 || $degrees < 1) { + if ($degrees < 1) { return Functions::NAN(); } @@ -145,13 +145,13 @@ class ChiSquared $degrees = Functions::flattenSingleValue($degrees); try { - $probability = self::validateFloat($probability); + $probability = self::validateProbability($probability); $degrees = self::validateInt($degrees); } catch (Exception $e) { return $e->getMessage(); } - if ($probability < 0.0 || $probability > 1.0 || $degrees < 1) { + if ($degrees < 1) { return Functions::NAN(); } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php index aa487329..2ea28391 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php @@ -87,14 +87,14 @@ class Gamma extends GammaBase $beta = Functions::flattenSingleValue($beta); try { - $probability = self::validateFloat($probability); + $probability = self::validateProbability($probability); $alpha = self::validateFloat($alpha); $beta = self::validateFloat($beta); } catch (Exception $e) { return $e->getMessage(); } - if (($alpha <= 0.0) || ($beta <= 0.0) || ($probability < 0.0) || ($probability > 1.0)) { + if (($alpha <= 0.0) || ($beta <= 0.0)) { return Functions::NAN(); } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php index d4025f6f..26211672 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/NewtonRaphson.php @@ -15,7 +15,7 @@ class NewtonRaphson $this->callback = $callback; } - public function execute($probability) + public function execute(float $probability) { $xLo = 100; $xHi = 0; diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php index a6d23c6b..ed02fe4d 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php @@ -59,13 +59,13 @@ class StudentT $degrees = Functions::flattenSingleValue($degrees); try { - $probability = self::validateFloat($probability); + $probability = self::validateProbability($probability); $degrees = self::validateInt($degrees); } catch (Exception $e) { return $e->getMessage(); } - if ($probability < 0.0 || $probability > 1.0 || $degrees <= 0) { + if ($degrees <= 0) { return Functions::NAN(); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/BinomDistRangeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/BinomDistRangeTest.php new file mode 100644 index 00000000..8db391e1 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/BinomDistRangeTest.php @@ -0,0 +1,31 @@ + Date: Wed, 31 Mar 2021 21:45:06 +0200 Subject: [PATCH 76/89] Extract a few more Distribution functions from Statistical (#1975) * Extract a few more Distribution functions from Statistical; this time EXPONDIST() and HYPGEOMDIST() * Extract the F Distribution (although only F.DIST() is implemented so far * Updae docblocks * PHPCS --- .../Calculation/Calculation.php | 8 +- .../Calculation/Statistical.php | 129 ++++++------------ .../Statistical/Distributions/Exponential.php | 49 +++++++ .../Statistical/Distributions/F.php | 59 ++++++++ .../Distributions/HyperGeometric.php | 56 ++++++++ .../{FDist2Test.php => FDistTest.php} | 10 +- .../Calculation/Statistical/EXPONDIST.php | 20 +++ tests/data/Calculation/Statistical/FDIST.php | 80 +++++++++++ tests/data/Calculation/Statistical/FDIST2.php | 17 --- .../Calculation/Statistical/HYPGEOMDIST.php | 34 ++++- 10 files changed, 344 insertions(+), 118 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php rename tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/{FDist2Test.php => FDistTest.php} (70%) create mode 100644 tests/data/Calculation/Statistical/FDIST.php delete mode 100644 tests/data/Calculation/Statistical/FDIST2.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 2026ce62..ef1be8c2 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -980,12 +980,12 @@ class Calculation ], 'EXPONDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'EXPONDIST'], + 'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'], 'argumentCount' => '3', ], 'EXPON.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'EXPONDIST'], + 'functionCall' => [Statistical\Distributions\Exponential::class, 'distribution'], 'argumentCount' => '3', ], 'FACT' => [ @@ -1010,7 +1010,7 @@ class Calculation ], 'F.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'FDIST2'], + 'functionCall' => [Statistical\Distributions\F::class, 'distribution'], 'argumentCount' => '4', ], 'F.DIST.RT' => [ @@ -1248,7 +1248,7 @@ class Calculation ], 'HYPGEOMDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'HYPGEOMDIST'], + 'functionCall' => [Statistical\Distributions\HyperGeometric::class, 'distribution'], 'argumentCount' => '4', ], 'HYPGEOM.DIST' => [ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index bfa00918..1576af98 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -116,12 +116,12 @@ class Statistical * * @Deprecated 1.17.0 * + * @see Statistical\Averages::averageDeviations() + * Use the averageDeviations() method in the Statistical\Averages class instead + * * @param mixed ...$args Data values * * @return float|string - * - *@see Statistical\Averages::averageDeviations() - * Use the averageDeviations() method in the Statistical\Averages class instead */ public static function AVEDEV(...$args) { @@ -160,12 +160,12 @@ class Statistical * * @Deprecated 1.17.0 * + * @see Statistical\Averages::averageA() + * Use the averageA() method in the Statistical\Averages class instead + * * @param mixed ...$args Data values * * @return float|string - * - *@see Statistical\Averages::averageA() - * Use the averageA() method in the Statistical\Averages class instead */ public static function AVERAGEA(...$args) { @@ -203,7 +203,7 @@ class Statistical * * @Deprecated 1.18.0 * - *@see Statistical\Distributions\Beta::distribution() + * @see Statistical\Distributions\Beta::distribution() * Use the distribution() method in the Statistical\Distributions\Beta class instead * * @param float $value Value at which you want to evaluate the distribution @@ -498,11 +498,6 @@ class Statistical * @param float $alpha criterion value * * @return int|string - * - * @TODO Warning. This implementation differs from the algorithm detailed on the MS - * web site in that $CumPGuessMinus1 = $CumPGuess - 1 rather than $CumPGuess - $PGuess - * This eliminates a potential endless loop error, but may have an adverse affect on the - * accuracy of the function (although all my tests have so far returned correct results). */ public static function CRITBINOM($trials, $probability, $alpha) { @@ -568,6 +563,11 @@ class Statistical * such as how long an automated bank teller takes to deliver cash. For example, you can * use EXPONDIST to determine the probability that the process takes at most 1 minute. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Exponential::distribution() + * Use the distribution() method in the Statistical\Distributions\Exponential class instead + * * @param float $value Value of the function * @param float $lambda The parameter value * @param bool $cumulative @@ -576,24 +576,7 @@ class Statistical */ public static function EXPONDIST($value, $lambda, $cumulative) { - $value = Functions::flattenSingleValue($value); - $lambda = Functions::flattenSingleValue($lambda); - $cumulative = Functions::flattenSingleValue($cumulative); - - if ((is_numeric($value)) && (is_numeric($lambda))) { - if (($value < 0) || ($lambda < 0)) { - return Functions::NAN(); - } - if ((is_numeric($cumulative)) || (is_bool($cumulative))) { - if ($cumulative) { - return 1 - exp(0 - $value * $lambda); - } - - return $lambda * exp(0 - $value * $lambda); - } - } - - return Functions::VALUE(); + return Statistical\Distributions\Exponential::distribution($value, $lambda, $cumulative); } /** @@ -604,6 +587,11 @@ class Statistical * For example, you can examine the test scores of men and women entering high school, and determine * if the variability in the females is different from that found in the males. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\F::distribution() + * Use the distribution() method in the Statistical\Distributions\Exponential class instead + * * @param float $value Value of the function * @param int $u The numerator degrees of freedom * @param int $v The denominator degrees of freedom @@ -614,34 +602,7 @@ class Statistical */ public static function FDIST2($value, $u, $v, $cumulative) { - $value = Functions::flattenSingleValue($value); - $u = Functions::flattenSingleValue($u); - $v = Functions::flattenSingleValue($v); - $cumulative = Functions::flattenSingleValue($cumulative); - - if (is_numeric($value) && is_numeric($u) && is_numeric($v)) { - if ($value < 0 || $u < 1 || $v < 1) { - return Functions::NAN(); - } - - $cumulative = (bool) $cumulative; - $u = (int) $u; - $v = (int) $v; - - if ($cumulative) { - $adjustedValue = ($u * $value) / ($u * $value + $v); - - return Statistical\Distributions\Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2); - } - - return (Statistical\Distributions\Gamma::gammaValue(($v + $u) / 2) / - (Statistical\Distributions\Gamma::gammaValue($u / 2) * - Statistical\Distributions\Gamma::gammaValue($v / 2))) * - (($u / $v) ** ($u / 2)) * - (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2))); - } - - return Functions::VALUE(); + return Statistical\Distributions\F::distribution($value, $u, $v, $cumulative); } /** @@ -908,42 +869,26 @@ class Statistical * Returns the hypergeometric distribution. HYPGEOMDIST returns the probability of a given number of * sample successes, given the sample size, population successes, and population size. * - * @param float $sampleSuccesses Number of successes in the sample - * @param float $sampleNumber Size of the sample - * @param float $populationSuccesses Number of successes in the population - * @param float $populationNumber Population size + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\HyperGeometric::distribution() + * Use the distribution() method in the Statistical\Distributions\HyperGeometric class instead + * + * @param mixed (int) $sampleSuccesses Number of successes in the sample + * @param mixed (int) $sampleNumber Size of the sample + * @param mixed (int) $populationSuccesses Number of successes in the population + * @param mixed (int) $populationNumber Population size * * @return float|string */ public static function HYPGEOMDIST($sampleSuccesses, $sampleNumber, $populationSuccesses, $populationNumber) { - $sampleSuccesses = Functions::flattenSingleValue($sampleSuccesses); - $sampleNumber = Functions::flattenSingleValue($sampleNumber); - $populationSuccesses = Functions::flattenSingleValue($populationSuccesses); - $populationNumber = Functions::flattenSingleValue($populationNumber); - - if ((is_numeric($sampleSuccesses)) && (is_numeric($sampleNumber)) && (is_numeric($populationSuccesses)) && (is_numeric($populationNumber))) { - $sampleSuccesses = floor($sampleSuccesses); - $sampleNumber = floor($sampleNumber); - $populationSuccesses = floor($populationSuccesses); - $populationNumber = floor($populationNumber); - - if (($sampleSuccesses < 0) || ($sampleSuccesses > $sampleNumber) || ($sampleSuccesses > $populationSuccesses)) { - return Functions::NAN(); - } - if (($sampleNumber <= 0) || ($sampleNumber > $populationNumber)) { - return Functions::NAN(); - } - if (($populationSuccesses <= 0) || ($populationSuccesses > $populationNumber)) { - return Functions::NAN(); - } - - return MathTrig::COMBIN($populationSuccesses, $sampleSuccesses) * - MathTrig::COMBIN($populationNumber - $populationSuccesses, $sampleNumber - $sampleSuccesses) / - MathTrig::COMBIN($populationNumber, $sampleNumber); - } - - return Functions::VALUE(); + return Statistical\Distributions\HyperGeometric::distribution( + $sampleSuccesses, + $sampleNumber, + $populationSuccesses, + $populationNumber + ); } /** @@ -2148,8 +2093,10 @@ class Statistical /** * ZTEST. * - * Returns the Weibull distribution. Use this distribution in reliability - * analysis, such as calculating a device's mean time to failure. + * Returns the one-tailed P-value of a z-test. + * + * For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be + * greater than the average of observations in the data set (array) — that is, the observed sample mean. * * @param float $dataSet * @param float $m0 Alpha Parameter diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php new file mode 100644 index 00000000..fe76816d --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php @@ -0,0 +1,49 @@ +getMessage(); + } + + if (($value < 0) || ($lambda < 0)) { + return Functions::NAN(); + } + + if ($cumulative === true) { + return 1 - exp(0 - $value * $lambda); + } + + return $lambda * exp(0 - $value * $lambda); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php new file mode 100644 index 00000000..84456873 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php @@ -0,0 +1,59 @@ +getMessage(); + } + + if ($value < 0 || $u < 1 || $v < 1) { + return Functions::NAN(); + } + + if ($cumulative) { + $adjustedValue = ($u * $value) / ($u * $value + $v); + + return Beta::incompleteBeta($adjustedValue, $u / 2, $v / 2); + } + + return (Gamma::gammaValue(($v + $u) / 2) / + (Gamma::gammaValue($u / 2) * Gamma::gammaValue($v / 2))) * + (($u / $v) ** ($u / 2)) * + (($value ** (($u - 2) / 2)) / ((1 + ($u / $v) * $value) ** (($u + $v) / 2))); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php new file mode 100644 index 00000000..e9848ed4 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php @@ -0,0 +1,56 @@ +getMessage(); + } + + if (($sampleSuccesses < 0) || ($sampleSuccesses > $sampleNumber) || ($sampleSuccesses > $populationSuccesses)) { + return Functions::NAN(); + } + if (($sampleNumber <= 0) || ($sampleNumber > $populationNumber)) { + return Functions::NAN(); + } + if (($populationSuccesses <= 0) || ($populationSuccesses > $populationNumber)) { + return Functions::NAN(); + } + + return MathTrig::COMBIN($populationSuccesses, $sampleSuccesses) * + MathTrig::COMBIN($populationNumber - $populationSuccesses, $sampleNumber - $sampleSuccesses) / + MathTrig::COMBIN($populationNumber, $sampleNumber); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FDist2Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FDistTest.php similarity index 70% rename from tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FDist2Test.php rename to tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FDistTest.php index a6e34429..525247f6 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FDist2Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FDistTest.php @@ -5,21 +5,21 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PHPUnit\Framework\TestCase; -class FDist2Test extends TestCase +class FDistTest extends TestCase { /** - * @dataProvider providerFDIST2 + * @dataProvider providerFDIST * * @param mixed $expectedResult */ - public function testFDIST2($expectedResult, ...$args): void + public function testFDIST($expectedResult, ...$args): void { $result = Statistical::FDIST2(...$args); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } - public function providerFDIST2(): array + public function providerFDIST(): array { - return require 'tests/data/Calculation/Statistical/FDIST2.php'; + return require 'tests/data/Calculation/Statistical/FDIST.php'; } } diff --git a/tests/data/Calculation/Statistical/EXPONDIST.php b/tests/data/Calculation/Statistical/EXPONDIST.php index df150e19..fda340db 100644 --- a/tests/data/Calculation/Statistical/EXPONDIST.php +++ b/tests/data/Calculation/Statistical/EXPONDIST.php @@ -1,6 +1,14 @@ Date: Thu, 1 Apr 2021 13:25:05 +0200 Subject: [PATCH 77/89] Resolution for [#Issue 1972](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) (#1978) * Resolution for [#Issue 1972](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) where format masks with a leading and trailing quote were always treated as literal strings, even when they masks containing quoted characters. Also resolves issue with colour name case-sensitivity --- CHANGELOG.md | 1 + .../Style/NumberFormat/Formatter.php | 4 +-- tests/data/Style/NumberFormat.php | 31 +++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb3ab111..69d1652a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ### Fixed +- Fixed issue with quoted strings in number format mask rendered with toFormattedString() [Issue 1972#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1972) [PR #1978](https://github.com/PHPOffice/PhpSpreadsheet/pull/1978) - Fixed issue with percentage formats in number format mask rendered with toFormattedString() [Issue 1929#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1929) [PR #1928](https://github.com/PHPOffice/PhpSpreadsheet/pull/1928) - Fixed issue with _ spacing character in number format mask corrupting output from toFormattedString() [Issue 1924#](https://github.com/PHPOffice/PhpSpreadsheet/issues/1924) [PR #1927](https://github.com/PHPOffice/PhpSpreadsheet/pull/1927) - Fix for [Issue #1887](https://github.com/PHPOffice/PhpSpreadsheet/issues/1887) - Lose Track of Selected Cells After Save diff --git a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php index 6fa43fe2..ef756d7b 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/Formatter.php @@ -43,7 +43,7 @@ class Formatter // 3 sections: [POSITIVE/TEXT] [NEGATIVE] [ZERO] // 4 sections: [POSITIVE] [NEGATIVE] [ZERO] [TEXT] $cnt = count($sections); - $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . ')\\]/'; + $color_regex = '/\\[(' . implode('|', Color::NAMED_COLORS) . ')\\]/mui'; $cond_regex = '/\\[(>|>=|<|<=|=|<>)([+-]?\\d+([.]\\d+)?)\\]/'; $colors = ['', '', '', '', '']; $condops = ['', '', '', '', '']; @@ -139,7 +139,7 @@ class Formatter // datetime format $value = DateFormatter::format($value, $format); } else { - if (substr($format, 0, 1) === '"' && substr($format, -1, 1) === '"') { + if (substr($format, 0, 1) === '"' && substr($format, -1, 1) === '"' && substr_count($format, '"') === 2) { $value = substr($format, 1, -1); } elseif (preg_match('/[0#, ]%/', $format)) { // % number format diff --git a/tests/data/Style/NumberFormat.php b/tests/data/Style/NumberFormat.php index f9db1732..aee357a7 100644 --- a/tests/data/Style/NumberFormat.php +++ b/tests/data/Style/NumberFormat.php @@ -390,6 +390,11 @@ return [ 12345, '[Green]General', ], + [ + '12345', + 12345, + '[GrEeN]General', + ], [ '-70', -70, @@ -404,14 +409,24 @@ return [ [ '12345', 12345, - '[Blue]0;[Red]0', + '[Blue]0;[Red]0-', ], + [ + '12345-', + -12345, + '[BLUE]0;[red]0-', + ], + [ + '12345-', + -12345, + '[blue]0;[RED]0-', + ], + // Multiple colors with text substitution [ 'Positive', 12, '[Green]"Positive";[Red]"Negative";[Blue]"Zero"', ], - // Multiple colors with text substitution [ 'Zero', 0, @@ -422,6 +437,7 @@ return [ -2, '[Green]"Positive";[Red]"Negative";[Blue]"Zero"', ], + // Value break points [ '<=3500 red', 3500, @@ -442,6 +458,17 @@ return [ 25, '[Green][<>25]"<>25 green";[Red]"else red"', ], + // Leading/trailing quotes in mask + [ + '$12.34 ', + 12.34, + '$#,##0.00_;[RED]"($"#,##0.00")"', + ], + [ + '($12.34)', + -12.34, + '$#,##0.00_;[RED]"($"#,##0.00")"', + ], [ 'pfx. 25.00', 25, From a4982fd9fedc392d8aaa2e90161eebfc9ff43b4b Mon Sep 17 00:00:00 2001 From: oleibman Date: Fri, 2 Apr 2021 05:35:34 -0700 Subject: [PATCH 78/89] Continue MathTrig Breakup - Penultimate? (#1973) * Continue MathTrig Breakup - Penultimate? Continuing the process of breaking MathTrip.php up into smaller classes. This round takes care of about half of what is left, so perhaps one round after this one will finish the job: - ARABIC - COMBIN; also implemented COMBINA - FACTDOUBLE - GCD (which accepts and ignores empty cells as arguments, but returns VALUE if all the arguments are that way; LCM does the same) - LOG_BASE, LOG10, LN - implemented MUNIT - MOD - POWER - RAND, RANDBETWEEN (RANDARRAY is too complicated to implement with this ticket) As you can see from the description, there are some functions which were combined in a single class. When not combined, I adopted PowerKiki's suggestion of using "execute" as the function name. Co-authored-by: Mark Baker --- .../Calculation/Calculation.php | 26 +- src/PhpSpreadsheet/Calculation/MathTrig.php | 223 +++--------------- .../Calculation/MathTrig/Arabic.php | 95 ++++++++ .../Calculation/MathTrig/Combinations.php | 74 ++++++ .../Calculation/MathTrig/FactDouble.php | 39 +++ .../Calculation/MathTrig/Gcd.php | 69 ++++++ .../Calculation/MathTrig/Helpers.php | 18 +- .../Calculation/MathTrig/Lcm.php | 33 ++- .../Calculation/MathTrig/Logarithms.php | 79 +++++++ .../Calculation/MathTrig/MatrixFunctions.php | 24 ++ .../Calculation/MathTrig/Mod.php | 36 +++ .../Calculation/MathTrig/Power.php | 42 ++++ .../Calculation/MathTrig/Random.php | 39 +++ .../Calculation/functionlist.txt | 2 + .../Functions/MathTrig/ArabicTest.php | 19 +- .../Functions/MathTrig/CombinATest.php | 33 +++ .../Functions/MathTrig/CombinTest.php | 28 ++- .../Functions/MathTrig/FactDoubleTest.php | 19 +- .../Functions/MathTrig/GcdTest.php | 27 ++- .../Calculation/Functions/MathTrig/LnTest.php | 29 +-- .../Functions/MathTrig/Log10Test.php | 29 +-- .../Functions/MathTrig/LogTest.php | 32 ++- .../Functions/MathTrig/MMultTest.php | 11 + .../Functions/MathTrig/MUnitTest.php | 23 ++ .../Functions/MathTrig/ModTest.php | 32 ++- .../Functions/MathTrig/MovedFunctionsTest.php | 13 +- .../Functions/MathTrig/PowerTest.php | 32 ++- .../Functions/MathTrig/RandBetweenTest.php | 46 ++++ .../Functions/MathTrig/RandTest.php | 24 ++ tests/data/Calculation/MathTrig/COMBIN.php | 9 + tests/data/Calculation/MathTrig/COMBINA.php | 135 +++++++++++ tests/data/Calculation/MathTrig/GCD.php | 4 + tests/data/Calculation/MathTrig/LCM.php | 2 + tests/data/Calculation/MathTrig/LN.php | 4 +- tests/data/Calculation/MathTrig/LOG.php | 1 + tests/data/Calculation/MathTrig/LOG10.php | 4 +- tests/data/Calculation/MathTrig/MDETERM.php | 30 +-- tests/data/Calculation/MathTrig/MINVERSE.php | 46 ++-- tests/data/Calculation/MathTrig/MOD.php | 9 +- tests/data/Calculation/MathTrig/POWER.php | 2 + .../data/Calculation/MathTrig/RANDBETWEEN.php | 19 ++ 41 files changed, 1089 insertions(+), 372 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/FactDouble.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Mod.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Power.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Random.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinATest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandTest.php create mode 100644 tests/data/Calculation/MathTrig/COMBINA.php create mode 100644 tests/data/Calculation/MathTrig/RANDBETWEEN.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index ef1be8c2..0538e28f 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -288,7 +288,7 @@ class Calculation ], 'ARABIC' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'ARABIC'], + 'functionCall' => [MathTrig\Arabic::class, 'evaluate'], 'argumentCount' => '1', ], 'AREAS' => [ @@ -555,12 +555,12 @@ class Calculation ], 'COMBIN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'COMBIN'], + 'functionCall' => [MathTrig\Combinations::class, 'withoutRepetition'], 'argumentCount' => '2', ], 'COMBINA' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [MathTrig\Combinations::class, 'withRepetition'], 'argumentCount' => '2', ], 'COMPLEX' => [ @@ -995,7 +995,7 @@ class Calculation ], 'FACTDOUBLE' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'FACTDOUBLE'], + 'functionCall' => [MathTrig\FactDouble::class, 'evaluate'], 'argumentCount' => '1', ], 'FALSE' => [ @@ -1187,7 +1187,7 @@ class Calculation ], 'GCD' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'GCD'], + 'functionCall' => [MathTrig\Gcd::class, 'evaluate'], 'argumentCount' => '1+', ], 'GEOMEAN' => [ @@ -1566,17 +1566,17 @@ class Calculation ], 'LN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinLN'], + 'functionCall' => [MathTrig\Logarithms::class, 'natural'], 'argumentCount' => '1', ], 'LOG' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'logBase'], + 'functionCall' => [MathTrig\Logarithms::class, 'withBase'], 'argumentCount' => '1,2', ], 'LOG10' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinLOG10'], + 'functionCall' => [MathTrig\Logarithms::class, 'base10'], 'argumentCount' => '1', ], 'LOGEST' => [ @@ -1701,7 +1701,7 @@ class Calculation ], 'MOD' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'MOD'], + 'functionCall' => [MathTrig\Mod::class, 'evaluate'], 'argumentCount' => '2', ], 'MODE' => [ @@ -1736,7 +1736,7 @@ class Calculation ], 'MUNIT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'functionCall' => [MathTrig\MatrixFunctions::class, 'funcMUnit'], 'argumentCount' => '1', ], 'N' => [ @@ -1973,7 +1973,7 @@ class Calculation ], 'POWER' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'POWER'], + 'functionCall' => [MathTrig\Power::class, 'evaluate'], 'argumentCount' => '2', ], 'PPMT' => [ @@ -2043,7 +2043,7 @@ class Calculation ], 'RAND' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'RAND'], + 'functionCall' => [MathTrig\Random::class, 'randNoArgs'], 'argumentCount' => '0', ], 'RANDARRAY' => [ @@ -2053,7 +2053,7 @@ class Calculation ], 'RANDBETWEEN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'RAND'], + 'functionCall' => [MathTrig\Random::class, 'randBetween'], 'argumentCount' => '2', ], 'RANK' => [ diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index 1dbc2854..e960d7ef 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -2,22 +2,15 @@ namespace PhpOffice\PhpSpreadsheet\Calculation; -use Exception; - class MathTrig { - private static function strSplit(string $roman): array - { - $rslt = str_split($roman); - - return is_array($rslt) ? $rslt : []; - } - /** * ARABIC. * * Converts a Roman numeral to an Arabic numeral. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Arabic class instead + * * Excel Function: * ARABIC(text) * @@ -27,69 +20,7 @@ class MathTrig */ public static function ARABIC($roman) { - // An empty string should return 0 - $roman = substr(trim(strtoupper((string) Functions::flattenSingleValue($roman))), 0, 255); - if ($roman === '') { - return 0; - } - - // Convert the roman numeral to an arabic number - $negativeNumber = $roman[0] === '-'; - if ($negativeNumber) { - $roman = substr($roman, 1); - } - - try { - $arabic = self::calculateArabic(self::strSplit($roman)); - } catch (Exception $e) { - return Functions::VALUE(); // Invalid character detected - } - - if ($negativeNumber) { - $arabic *= -1; // The number should be negative - } - - return $arabic; - } - - /** - * Recursively calculate the arabic value of a roman numeral. - * - * @param int $sum - * @param int $subtract - * - * @return int - */ - protected static function calculateArabic(array $roman, &$sum = 0, $subtract = 0) - { - $lookup = [ - 'M' => 1000, - 'D' => 500, - 'C' => 100, - 'L' => 50, - 'X' => 10, - 'V' => 5, - 'I' => 1, - ]; - - $numeral = array_shift($roman); - if (!isset($lookup[$numeral])) { - throw new Exception('Invalid character detected'); - } - - $arabic = $lookup[$numeral]; - if (count($roman) > 0 && isset($lookup[$roman[0]]) && $arabic < $lookup[$roman[0]]) { - $subtract += $arabic; - } else { - $sum += ($arabic - $subtract); - $subtract = 0; - } - - if (count($roman) > 0) { - self::calculateArabic($roman, $sum, $subtract); - } - - return $sum; + return MathTrig\Arabic::evaluate($roman); } /** @@ -172,6 +103,8 @@ class MathTrig * 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. * + * @Deprecated 2.0.0 Use the without method in the MathTrig\Combinations class instead + * * Excel Function: * COMBIN(numObjs,numInSet) * @@ -182,20 +115,7 @@ class MathTrig */ public static function COMBIN($numObjs, $numInSet) { - $numObjs = Functions::flattenSingleValue($numObjs); - $numInSet = Functions::flattenSingleValue($numInSet); - - if ((is_numeric($numObjs)) && (is_numeric($numInSet))) { - if ($numObjs < $numInSet) { - return Functions::NAN(); - } elseif ($numInSet < 0) { - return Functions::NAN(); - } - - return round(MathTrig\Fact::funcFact($numObjs) / MathTrig\Fact::funcFact($numObjs - $numInSet)) / MathTrig\Fact::funcFact($numInSet); - } - - return Functions::VALUE(); + return MathTrig\Combinations::withoutRepetition($numObjs, $numInSet); } /** @@ -256,6 +176,8 @@ class MathTrig * * Returns the double factorial of a number. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\FactDouble class instead + * * Excel Function: * FACTDOUBLE(factVal) * @@ -265,23 +187,7 @@ class MathTrig */ public static function FACTDOUBLE($factVal) { - $factLoop = Functions::flattenSingleValue($factVal); - - if (is_numeric($factLoop)) { - $factLoop = floor($factLoop); - if ($factVal < 0) { - return Functions::NAN(); - } - $factorial = 1; - while ($factLoop > 1) { - $factorial *= $factLoop--; - --$factLoop; - } - - return $factorial; - } - - return Functions::VALUE(); + return MathTrig\FactDouble::evaluate($factVal); } /** @@ -351,11 +257,6 @@ class MathTrig return MathTrig\FloorPrecise::funcFloorPrecise($number, $significance); } - private static function evaluateGCD($a, $b) - { - return $b ? self::evaluateGCD($b, $a % $b) : $a; - } - /** * INT. * @@ -384,6 +285,8 @@ class MathTrig * The greatest common divisor is the largest integer that divides both * number1 and number2 without a remainder. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Gcd class instead + * * Excel Function: * GCD(number1[,number2[, ...]]) * @@ -393,22 +296,7 @@ class MathTrig */ public static function GCD(...$args) { - $args = Functions::flattenArray($args); - // Loop through arguments - foreach (Functions::flattenArray($args) as $value) { - if (!is_numeric($value)) { - return Functions::VALUE(); - } elseif ($value < 0) { - return Functions::NAN(); - } - } - - $gcd = (int) array_pop($args); - do { - $gcd = self::evaluateGCD($gcd, (int) array_pop($args)); - } while (!empty($args)); - - return $gcd; + return MathTrig\Gcd::evaluate(...$args); } /** @@ -438,6 +326,8 @@ class MathTrig * * Returns the logarithm of a number to a specified base. The default base is 10. * + * @Deprecated 2.0.0 Use the withBase method in the MathTrig\Logarithms class instead + * * Excel Function: * LOG(number[,base]) * @@ -446,19 +336,9 @@ class MathTrig * * @return float|string The result, or a string containing an error */ - public static function logBase($number = null, $base = 10) + public static function logBase($number, $base = 10) { - $number = Functions::flattenSingleValue($number); - $base = ($base === null) ? 10 : (float) Functions::flattenSingleValue($base); - - if ((!is_numeric($base)) || (!is_numeric($number))) { - return Functions::VALUE(); - } - if (($base <= 0) || ($number <= 0)) { - return Functions::NAN(); - } - - return log($number, $base); + return MathTrig\Logarithms::withBase($number, $base); } /** @@ -517,6 +397,8 @@ class MathTrig /** * MOD. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Mod class instead + * * @param int $a Dividend * @param int $b Divisor * @@ -524,18 +406,7 @@ class MathTrig */ public static function MOD($a = 1, $b = 1) { - $a = (float) Functions::flattenSingleValue($a); - $b = (float) Functions::flattenSingleValue($b); - - if ($b == 0.0) { - return Functions::DIV0(); - } elseif (($a < 0.0) && ($b > 0.0)) { - return $b - fmod(abs($a), $b); - } elseif (($a > 0.0) && ($b < 0.0)) { - return $b + fmod($a, abs($b)); - } - - return fmod($a, $b); + return MathTrig\Mod::evaluate($a, $b); } /** @@ -562,6 +433,8 @@ class MathTrig * * Returns the ratio of the factorial of a sum of values to the product of factorials. * + * @Deprecated 2.0.0 Use the funcMultinomial method in the MathTrig\Multinomial class instead + * * @param mixed[] $args An array of mixed values for the Data Series * * @return float|string The result, or a string containing an error @@ -592,27 +465,16 @@ class MathTrig * * Computes x raised to the power y. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Power class instead + * * @param float $x * @param float $y * - * @return float|string The result, or a string containing an error + * @return float|int|string The result, or a string containing an error */ public static function POWER($x = 0, $y = 2) { - $x = Functions::flattenSingleValue($x); - $y = Functions::flattenSingleValue($y); - - // Validate parameters - if ($x == 0.0 && $y == 0.0) { - return Functions::NAN(); - } elseif ($x == 0.0 && $y < 0.0) { - return Functions::DIV0(); - } - - // Return - $result = $x ** $y; - - return (!is_nan($result) && !is_infinite($result)) ? $result : Functions::NAN(); + return MathTrig\Power::evaluate($x, $y); } /** @@ -656,23 +518,18 @@ class MathTrig } /** - * RAND. + * RAND/RANDBETWEEN. + * + * @Deprecated 2.0.0 Use the randNoArg or randBetween method in the MathTrig\Random class instead * * @param int $min Minimal value * @param int $max Maximal value * - * @return int Random number + * @return float|int|string Random number */ public static function RAND($min = 0, $max = 0) { - $min = Functions::flattenSingleValue($min); - $max = Functions::flattenSingleValue($max); - - if ($min == 0 && $max == 0) { - return (mt_rand(0, 10000000)) / 10000000; - } - - return mt_rand($min, $max); + return MathTrig\Random::randBetween($min, $max); } /** @@ -1388,19 +1245,15 @@ class MathTrig * * Returns the result of builtin function log after validating args. * + * @Deprecated 2.0.0 Use the natural method in the MathTrig\Logarithms class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinLN($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return log($number); + return MathTrig\Logarithms::natural($number); } /** @@ -1408,19 +1261,15 @@ class MathTrig * * Returns the result of builtin function log after validating args. * + * @Deprecated 2.0.0 Use the base10 method in the MathTrig\Logarithms class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinLOG10($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return log10($number); + return MathTrig\Logarithms::base10($number); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php b/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php new file mode 100644 index 00000000..320856b9 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Arabic.php @@ -0,0 +1,95 @@ + 1000, + 'D' => 500, + 'C' => 100, + 'L' => 50, + 'X' => 10, + 'V' => 5, + 'I' => 1, + ]; + + /** + * Recursively calculate the arabic value of a roman numeral. + * + * @param int $sum + * @param int $subtract + * + * @return int + */ + private static function calculateArabic(array $roman, &$sum = 0, $subtract = 0) + { + $numeral = array_shift($roman); + if (!isset(self::ROMAN_LOOKUP[$numeral])) { + throw new Exception('Invalid character detected'); + } + + $arabic = self::ROMAN_LOOKUP[$numeral]; + if (count($roman) > 0 && isset(self::ROMAN_LOOKUP[$roman[0]]) && $arabic < self::ROMAN_LOOKUP[$roman[0]]) { + $subtract += $arabic; + } else { + $sum += ($arabic - $subtract); + $subtract = 0; + } + + if (count($roman) > 0) { + self::calculateArabic($roman, $sum, $subtract); + } + + return $sum; + } + + private static function strSplit(string $roman): array + { + $rslt = str_split($roman); + + return is_array($rslt) ? $rslt : []; + } + + /** + * ARABIC. + * + * Converts a Roman numeral to an Arabic numeral. + * + * Excel Function: + * ARABIC(text) + * + * @param string $roman + * + * @return int|string the arabic numberal contrived from the roman numeral + */ + public static function evaluate($roman) + { + // An empty string should return 0 + $roman = substr(trim(strtoupper((string) Functions::flattenSingleValue($roman))), 0, 255); + if ($roman === '') { + return 0; + } + + // Convert the roman numeral to an arabic number + $negativeNumber = $roman[0] === '-'; + if ($negativeNumber) { + $roman = substr($roman, 1); + } + + try { + $arabic = self::calculateArabic(self::strSplit($roman)); + } catch (Exception $e) { + return Functions::VALUE(); // Invalid character detected + } + + if ($negativeNumber) { + $arabic *= -1; // The number should be negative + } + + return $arabic; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php b/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php new file mode 100644 index 00000000..78b18fc6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Combinations.php @@ -0,0 +1,74 @@ +getMessage(); + } + + return round(Fact::funcFact($numObjs) / Fact::funcFact($numObjs - $numInSet)) / Fact::funcFact($numInSet); + } + + /** + * COMBIN. + * + * 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) + * + * @param mixed $numObjs Number of different objects + * @param mixed $numInSet Number of objects in each combination + * + * @return float|int|string Number of combinations, or a string containing an error + */ + public static function withRepetition($numObjs, $numInSet) + { + try { + $numObjs = Helpers::validateNumericNullSubstitution($numObjs, null); + $numInSet = Helpers::validateNumericNullSubstitution($numInSet, null); + Helpers::validateNotNegative($numInSet); + Helpers::validateNotNegative($numObjs); + $numObjs = (int) $numObjs; + $numInSet = (int) $numInSet; + // Microsoft documentation says following is true, but Excel + // does not enforce this restriction. + //Helpers::validateNotNegative($numObjs - $numInSet); + if ($numObjs === 0) { + Helpers::validateNotNegative(-$numInSet); + + return 1; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return round(Fact::funcFact($numObjs + $numInSet - 1) / Fact::funcFact($numObjs - 1)) / Fact::funcFact($numInSet); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/FactDouble.php b/src/PhpSpreadsheet/Calculation/MathTrig/FactDouble.php new file mode 100644 index 00000000..4b760144 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/FactDouble.php @@ -0,0 +1,39 @@ +getMessage(); + } + + $factLoop = floor($factVal); + $factorial = 1; + while ($factLoop > 1) { + $factorial *= $factLoop; + $factLoop -= 2; + } + + return $factorial; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php b/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php new file mode 100644 index 00000000..21c22699 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Gcd.php @@ -0,0 +1,69 @@ +getMessage(); + } + + if (count($arrayArgs) <= 0) { + return Functions::VALUE(); + } + $gcd = (int) array_pop($arrayArgs); + do { + $gcd = self::evaluateGCD($gcd, (int) array_pop($arrayArgs)); + } while (!empty($arrayArgs)); + + return $gcd; + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php index 7abcf050..26716467 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Helpers.php @@ -66,13 +66,27 @@ class Helpers * * @param float|int $number */ - public static function validateNotNegative($number): void + public static function validateNotNegative($number, ?string $except = null): void { if ($number >= 0) { return; } - throw new Exception(Functions::NAN()); + throw new Exception($except ?? Functions::NAN()); + } + + /** + * Confirm number > 0. + * + * @param float|int $number + */ + public static function validatePositive($number, ?string $except = null): void + { + if ($number > 0) { + return; + } + + throw new Exception($except ?? Functions::NAN()); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php b/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php index 3d3f0e46..38d9b620 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Lcm.php @@ -53,12 +53,15 @@ class Lcm try { $arrayArgs = []; $anyZeros = 0; + $anyNonNulls = 0; foreach (Functions::flattenArray($args) as $value1) { + $anyNonNulls += (int) ($value1 !== null); $value = Helpers::validateNumericNullSubstitution($value1, 1); Helpers::validateNotNegative($value); $arrayArgs[] = (int) $value; $anyZeros += (int) !((bool) $value); } + self::testNonNulls($anyNonNulls); if ($anyZeros) { return 0; } @@ -76,15 +79,7 @@ class Lcm foreach ($myCountedFactors as $myCountedFactor => $myCountedPower) { $myPoweredFactors[$myCountedFactor] = $myCountedFactor ** $myCountedPower; } - foreach ($myPoweredFactors as $myPoweredValue => $myPoweredFactor) { - if (isset($allPoweredFactors[$myPoweredValue])) { - if ($allPoweredFactors[$myPoweredValue] < $myPoweredFactor) { - $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; - } - } else { - $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; - } - } + self::processPoweredFactors($allPoweredFactors, $myPoweredFactors); } foreach ($allPoweredFactors as $allPoweredFactor) { $returnValue *= (int) $allPoweredFactor; @@ -92,4 +87,24 @@ class Lcm return $returnValue; } + + private static function processPoweredFactors(array &$allPoweredFactors, array &$myPoweredFactors): void + { + foreach ($myPoweredFactors as $myPoweredValue => $myPoweredFactor) { + if (isset($allPoweredFactors[$myPoweredValue])) { + if ($allPoweredFactors[$myPoweredValue] < $myPoweredFactor) { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } else { + $allPoweredFactors[$myPoweredValue] = $myPoweredFactor; + } + } + } + + private static function testNonNulls(int $anyNonNulls): void + { + if (!$anyNonNulls) { + throw new Exception(Functions::VALUE()); + } + } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php b/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php new file mode 100644 index 00000000..356a0937 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Logarithms.php @@ -0,0 +1,79 @@ +getMessage(); + } + + return log($number, $base); + } + + /** + * LOG10. + * + * Returns the result of builtin function log after validating args. + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function base10($number) + { + try { + $number = Helpers::validateNumericNullBool($number); + Helpers::validatePositive($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return log10($number); + } + + /** + * LN. + * + * Returns the result of builtin function log after validating args. + * + * @Deprecated 2.0.0 Use the natural method in the MathTrig\Logarithms class instead + * + * @param mixed $number Should be numeric + * + * @return float|string Rounded number + */ + public static function natural($number) + { + try { + $number = Helpers::validateNumericNullBool($number); + Helpers::validatePositive($number); + } catch (Exception $e) { + return $e->getMessage(); + } + + return log($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php b/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php index 77c8b1e1..145adfa0 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig/MatrixFunctions.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig; use Exception; +use Matrix\Builder; use Matrix\Exception as MatrixException; use Matrix\Matrix; use PhpOffice\PhpSpreadsheet\Calculation\Functions; @@ -111,4 +112,27 @@ class MatrixFunctions return $e->getMessage(); } } + + /** + * MUnit. + * + * @param mixed $dimension Number of rows and columns + * + * @return array|string The result, or a string containing an error + */ + public static function funcMUnit($dimension) + { + try { + $dimension = (int) Helpers::validateNumericNullBool($dimension); + Helpers::validatePositive($dimension, Functions::VALUE()); + $matrix = Builder::createFilledMatrix(0, $dimension)->toArray(); + for ($x = 0; $x < $dimension; ++$x) { + $matrix[$x][$x] = 1; + } + + return $matrix; + } catch (Exception $e) { + return $e->getMessage(); + } + } } diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Mod.php b/src/PhpSpreadsheet/Calculation/MathTrig/Mod.php new file mode 100644 index 00000000..b2e3cf4b --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Mod.php @@ -0,0 +1,36 @@ +getMessage(); + } + + if (($a < 0.0) && ($b > 0.0)) { + return $b - fmod(abs($a), $b); + } + if (($a > 0.0) && ($b < 0.0)) { + return $b + fmod($a, abs($b)); + } + + return fmod($a, $b); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Power.php b/src/PhpSpreadsheet/Calculation/MathTrig/Power.php new file mode 100644 index 00000000..70d177cd --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Power.php @@ -0,0 +1,42 @@ +getMessage(); + } + + // Validate parameters + if (!$x && !$y) { + return Functions::NAN(); + } + if (!$x && $y < 0.0) { + return Functions::DIV0(); + } + + // Return + $result = $x ** $y; + + return Helpers::numberOrNan($result); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Random.php b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php new file mode 100644 index 00000000..1a384fe9 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Random.php @@ -0,0 +1,39 @@ +getMessage(); + } + + return mt_rand($min, $max); + } +} diff --git a/src/PhpSpreadsheet/Calculation/functionlist.txt b/src/PhpSpreadsheet/Calculation/functionlist.txt index 96c28a97..270715cd 100644 --- a/src/PhpSpreadsheet/Calculation/functionlist.txt +++ b/src/PhpSpreadsheet/Calculation/functionlist.txt @@ -53,6 +53,7 @@ CODE COLUMN COLUMNS COMBIN +COMBINA COMPLEX CONCAT CONCATENATE @@ -249,6 +250,7 @@ MODE MONTH MROUND MULTINOMIAL +MUNIT N NA NEGBINOMDIST diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ArabicTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ArabicTest.php index 7b3a5e15..93ed4f25 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ArabicTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ArabicTest.php @@ -2,17 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class ArabicTest extends TestCase +class ArabicTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerARABIC * @@ -21,8 +12,12 @@ class ArabicTest extends TestCase */ public function testARABIC($expectedResult, $romanNumeral): void { - $result = MathTrig::ARABIC($romanNumeral); - self::assertEquals($expectedResult, $result); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue($romanNumeral); + $sheet->getCell('B1')->setValue('=ARABIC(A1)'); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); } public function providerARABIC() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinATest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinATest.php new file mode 100644 index 00000000..6ab71c7c --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinATest.php @@ -0,0 +1,33 @@ +mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($numObjs !== null) { + $sheet->getCell('A1')->setValue($numObjs); + } + if ($numInSet !== null) { + $sheet->getCell('A2')->setValue($numInSet); + } + $sheet->getCell('B1')->setValue('=COMBINA(A1,A2)'); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); + } + + public function providerCOMBINA() + { + return require 'tests/data/Calculation/MathTrig/COMBINA.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinTest.php index d9156339..8b2749d8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/CombinTest.php @@ -2,26 +2,28 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class CombinTest extends TestCase +class CombinTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerCOMBIN * * @param mixed $expectedResult + * @param mixed $numObjs + * @param mixed $numInSet */ - public function testCOMBIN($expectedResult, ...$args): void + public function testCOMBIN($expectedResult, $numObjs, $numInSet): void { - $result = MathTrig::COMBIN(...$args); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($numObjs !== null) { + $sheet->getCell('A1')->setValue($numObjs); + } + if ($numInSet !== null) { + $sheet->getCell('A2')->setValue($numInSet); + } + $sheet->getCell('B1')->setValue('=COMBIN(A1,A2)'); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); } public function providerCOMBIN() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php index f0b6b146..f3627205 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php @@ -2,17 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class FactDoubleTest extends TestCase +class FactDoubleTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerFACTDOUBLE * @@ -21,8 +12,12 @@ class FactDoubleTest extends TestCase */ public function testFACTDOUBLE($expectedResult, $value): void { - $result = MathTrig::FACTDOUBLE($value); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $sheet->getCell('A1')->setValue($value); + $sheet->getCell('B1')->setValue('=FACTDOUBLE(A1)'); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEquals($expectedResult, $result); } public function providerFACTDOUBLE() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/GcdTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/GcdTest.php index ce1aec3f..6d87ccc3 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/GcdTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/GcdTest.php @@ -2,17 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class GcdTest extends TestCase +class GcdTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerGCD * @@ -20,7 +11,21 @@ class GcdTest extends TestCase */ public function testGCD($expectedResult, ...$args): void { - $result = MathTrig::GCD(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $row = 0; + foreach ($args as $arg) { + ++$row; + if ($arg !== null) { + $sheet->getCell("A$row")->setValue($arg); + } + } + if ($row < 1) { + $sheet->getCell('B1')->setValue('=GCD()'); + } else { + $sheet->getCell('B1')->setValue("=GCD(A1:A$row)"); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php index 1910ef02..e4c46017 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LnTest.php @@ -2,30 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class LnTest extends TestCase +class LnTest extends AllSetupTeardown { /** * @dataProvider providerLN * * @param mixed $expectedResult - * @param mixed $val + * @param mixed $number */ - public function testLN($expectedResult, $val = null): void + public function testLN($expectedResult, $number = 'omitted'): void { - if ($val === null) { - $this->expectException(CalcExp::class); - $formula = '=LN()'; - } else { - $formula = "=LN($val)"; + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($number !== null) { + $sheet->getCell('A1')->setValue($number); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); - $result = $sheet->getCell('A1')->getCalculatedValue(); + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=LN()'); + } else { + $sheet->getCell('B1')->setValue('=LN(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php index e537030c..b6afaeda 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/Log10Test.php @@ -2,30 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class Log10Test extends TestCase +class Log10Test extends AllSetupTeardown { /** * @dataProvider providerLN * * @param mixed $expectedResult - * @param mixed $val + * @param mixed $number */ - public function testLN($expectedResult, $val = null): void + public function testLN($expectedResult, $number = 'omitted'): void { - if ($val === null) { - $this->expectException(CalcExp::class); - $formula = '=LOG10()'; - } else { - $formula = "=LOG10($val)"; + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($number !== null) { + $sheet->getCell('A1')->setValue($number); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); - $result = $sheet->getCell('A1')->getCalculatedValue(); + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=LOG10()'); + } else { + $sheet->getCell('B1')->setValue('=LOG10(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LogTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LogTest.php index 184d83e6..f27ec94a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LogTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/LogTest.php @@ -2,25 +2,33 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class LogTest extends TestCase +class LogTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerLOG * * @param mixed $expectedResult + * @param mixed $number + * @param mixed $base */ - public function testLOG($expectedResult, ...$args): void + public function testLOG($expectedResult, $number = 'omitted', $base = 'omitted'): void { - $result = MathTrig::logBase(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($number !== null) { + $sheet->getCell('A1')->setValue($number); + } + if ($base !== null) { + $sheet->getCell('A2')->setValue($base); + } + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=LOG()'); + } elseif ($base === 'omitted') { + $sheet->getCell('B1')->setValue('=LOG(A1)'); + } else { + $sheet->getCell('B1')->setValue('=LOG(A1,A2)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php index ca8ee5d7..6c40103c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php @@ -28,5 +28,16 @@ class MMultTest extends AllSetupTeardown $sheet = $this->sheet; $sheet->getCell('A1')->setValue('=MMULT({1,2,3}, {1,2,3})'); // incompatible dimensions self::assertSame('#VALUE!', $sheet->getCell('A1')->getCalculatedValue()); + + $sheet->getCell('A11')->setValue('=MMULT({1, 2, 3, 4}, {5; 6; 7; 8})'); + self::assertEquals(70, $sheet->getCell('A11')->getCalculatedValue()); + $sheet->getCell('A2')->setValue(1); + $sheet->getCell('B2')->setValue(2); + $sheet->getCell('C2')->setValue(3); + $sheet->getCell('D2')->setValue(4); + $sheet->getCell('D3')->setValue(5); + $sheet->getCell('D4')->setValue(6); + $sheet->getCell('A12')->setValue('=MMULT(A2:C2,D2:D4)'); + self::assertEquals(32, $sheet->getCell('A12')->getCalculatedValue()); } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php new file mode 100644 index 00000000..4e9f95cf --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MUnitTest.php @@ -0,0 +1,23 @@ +mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($dividend !== null) { + $sheet->getCell('A1')->setValue($dividend); + } + if ($divisor !== null) { + $sheet->getCell('A2')->setValue($divisor); + } + if ($dividend === 'omitted') { + $sheet->getCell('B1')->setValue('=MOD()'); + } elseif ($divisor === 'omitted') { + $sheet->getCell('B1')->setValue('=MOD(A1)'); + } else { + $sheet->getCell('B1')->setValue('=MOD(A1,A2)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php index 5b3642ff..580092cd 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php @@ -20,6 +20,7 @@ class MovedFunctionsTest extends TestCase self::assertEqualsWithDelta(0, MathTrig::builtinACOSH(1), 1E-9); self::assertEqualsWithDelta(3.04192400109863, MathTrig::ACOT(-10), 1E-9); self::assertEqualsWithDelta(-0.20273255405408, MathTrig::ACOTH(-5), 1E-9); + self::assertSame(49, MathTrig::ARABIC('XLIX')); self::assertEqualsWithDelta(0, MathTrig::builtinASIN(0), 1E-9); self::assertEqualsWithDelta(0, MathTrig::builtinASINH(0), 1E-9); self::assertEqualsWithDelta(0, MathTrig::builtinATAN(0), 1E-9); @@ -27,6 +28,7 @@ class MovedFunctionsTest extends TestCase self::assertEqualsWithDelta('#DIV/0!', MathTrig::ATAN2(0, 0), 1E-9); self::assertEquals('12', MathTrig::BASE(10, 8)); self::assertEquals(-6, MathTrig::CEILING(-4.5, -2)); + self::assertEquals(15, MathTrig::COMBIN(6, 2)); self::assertEquals(1, MathTrig::builtinCOS(0)); self::assertEquals(1, MathTrig::builtinCOSH(0)); self::assertEquals('#DIV/0!', MathTrig::COT(0)); @@ -35,25 +37,32 @@ class MovedFunctionsTest extends TestCase self::assertEquals('#DIV/0!', MathTrig::CSCH(0)); self::assertEquals(6, MathTrig::EVEN(4.5)); self::assertEquals(6, MathTrig::FACT(3)); + self::assertEquals(105, MathTrig::FACTDOUBLE(7)); self::assertEquals(-6, MathTrig::FLOOR(-4.5, 2)); self::assertEquals(0.23, MathTrig::FLOORMATH(0.234, 0.01)); self::assertEquals(-4, MathTrig::FLOORPRECISE(-2.5, 2)); self::assertEquals(-9, MathTrig::INT(-8.3)); self::assertEquals(12, MathTrig::LCM(4, 6)); + self::assertEqualswithDelta(2.302585, MathTrig::builtinLN(10), 1E-6); + self::assertEqualswithDelta(0.306762486567556, MathTrig::logBase(1.5, 3.75), 1E-6); + self::assertEqualswithDelta(0.301030, MathTrig::builtinLOG10(2), 1E-6); self::assertEquals(1, MathTrig::MDETERM([1])); self::assertEquals( [[2, 2], [2, 1]], - MathTrig::MINVERSE([[-0.5, 1.0], [1.0, -1.0]]) + MathTrig::MINVERSE([[-0.5, 1.0], [1.0, -1.0]]) ); self::assertEquals( [[23], [53]], - MathTrig::MMULT([[1, 2], [3, 4]], [[7], [8]]) + MathTrig::MMULT([[1, 2], [3, 4]], [[7], [8]]) ); + self::assertEquals(1, MathTrig::MOD(5, 2)); self::assertEquals(6, MathTrig::MROUND(7.3, 3)); self::assertEquals(1, MathTrig::MULTINOMIAL(1)); self::assertEquals(5, MathTrig::ODD(4.5)); + self::assertEquals(8, MathTrig::POWER(2, 3)); self::assertEquals(8, MathTrig::PRODUCT(1, 2, 4)); self::assertEquals(8, MathTrig::QUOTIENT(17, 2)); + self::assertGreaterThanOrEqual(0, MATHTRIG::RAND()); self::assertEquals('I', MathTrig::ROMAN(1)); self::assertEquals(3.3, MathTrig::builtinROUND(3.27, 1)); self::assertEquals(662, MathTrig::ROUNDDOWN(662.79, 0)); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PowerTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PowerTest.php index 6749b14a..f68941b8 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PowerTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/PowerTest.php @@ -2,25 +2,33 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class PowerTest extends TestCase +class PowerTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerPOWER * * @param mixed $expectedResult + * @param mixed $base + * @param mixed $exponent */ - public function testPOWER($expectedResult, ...$args): void + public function testPOWER($expectedResult, $base = 'omitted', $exponent = 'omitted'): void { - $result = MathTrig::POWER(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($base !== null) { + $sheet->getCell('A1')->setValue($base); + } + if ($exponent !== null) { + $sheet->getCell('A2')->setValue($exponent); + } + if ($base === 'omitted') { + $sheet->getCell('B1')->setValue('=POWER()'); + } elseif ($exponent === 'omitted') { + $sheet->getCell('B1')->setValue('=POWER(A1)'); + } else { + $sheet->getCell('B1')->setValue('=POWER(A1,A2)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php new file mode 100644 index 00000000..0efe8ba6 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandBetweenTest.php @@ -0,0 +1,46 @@ +mightHaveException($expectedResult); + $sheet = $this->sheet; + $lower = (int) $min; + $upper = (int) $max; + if ($min !== null) { + $sheet->getCell('A1')->setValue($min); + } + if ($max !== null) { + $sheet->getCell('A2')->setValue($max); + } + if ($min === 'omitted') { + $sheet->getCell('B1')->setValue('=RANDBETWEEN()'); + } elseif ($max === 'omitted') { + $sheet->getCell('B1')->setValue('=RANDBETWEEN(A1)'); + } else { + $sheet->getCell('B1')->setValue('=RANDBETWEEN(A1,A2)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); + if (is_numeric($expectedResult)) { + self::assertGreaterThanOrEqual($lower, $result); + self::assertLessThanOrEqual($upper, $result); + } else { + self::assertSame($expectedResult, $result); + } + } + + public function providerRANDBETWEEN() + { + return require 'tests/data/Calculation/MathTrig/RANDBETWEEN.php'; + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandTest.php new file mode 100644 index 00000000..e5e2e107 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RandTest.php @@ -0,0 +1,24 @@ +sheet; + $sheet->getCell('B1')->setValue('=RAND()'); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertGreaterThanOrEqual(0, $result); + self::assertLessThanOrEqual(1, $result); + } + + public function testRandException(): void + { + $this->mightHaveException('exception'); + $sheet = $this->sheet; + $sheet->getCell('B1')->setValue('=RAND(A1)'); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEquals(0, $result); + } +} diff --git a/tests/data/Calculation/MathTrig/COMBIN.php b/tests/data/Calculation/MathTrig/COMBIN.php index f5e36a75..d163e883 100644 --- a/tests/data/Calculation/MathTrig/COMBIN.php +++ b/tests/data/Calculation/MathTrig/COMBIN.php @@ -71,6 +71,11 @@ return [ [ '#VALUE!', 'ABCD', + 2, + ], + [ + '#VALUE!', + 3, 'EFGH', ], [ @@ -123,4 +128,8 @@ return [ 6, 7, ], + [1, 0, 0], + ['#NUM!', 0, 1], + ['#VALUE!', 1, true], + ['#VALUE!', 1, null], ]; diff --git a/tests/data/Calculation/MathTrig/COMBINA.php b/tests/data/Calculation/MathTrig/COMBINA.php new file mode 100644 index 00000000..c5a26a5f --- /dev/null +++ b/tests/data/Calculation/MathTrig/COMBINA.php @@ -0,0 +1,135 @@ + Date: Fri, 2 Apr 2021 20:17:03 +0200 Subject: [PATCH 79/89] Extract Normal and Standard Normal Distributions from the Statistical Class (#1981) * Extract Normal and Standard Normal Distributions from the Statistical Class * Extract ZTest from the Statistical Class, and move it to the Standard Normal Distribution class Additional unit tests for NORMINV() * Extract LogNormal distribution functions from Statistical --- .../Calculation/Calculation.php | 28 +- .../Calculation/Statistical.php | 245 ++++-------------- .../Calculation/Statistical/Confidence.php | 3 +- .../Statistical/Distributions/LogNormal.php | 121 +++++++++ .../Statistical/Distributions/Normal.php | 168 ++++++++++++ .../Distributions/StandardNormal.php | 89 +++++++ .../Functions/Statistical/NormInvTest.php | 2 +- .../Functions/Statistical/ZTestTest.php | 28 ++ tests/data/Calculation/Statistical/LOGINV.php | 18 +- .../Calculation/Statistical/LOGNORMDIST.php | 8 +- .../Calculation/Statistical/LOGNORMDIST2.php | 9 +- .../data/Calculation/Statistical/NORMDIST.php | 5 +- .../data/Calculation/Statistical/NORMINV.php | 6 +- .../Calculation/Statistical/NORMSDIST.php | 2 +- .../Calculation/Statistical/NORMSDIST2.php | 3 +- .../data/Calculation/Statistical/NORMSINV.php | 3 + tests/data/Calculation/Statistical/ZTEST.php | 42 +++ 17 files changed, 563 insertions(+), 217 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php create mode 100644 src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/ZTestTest.php create mode 100644 tests/data/Calculation/Statistical/ZTEST.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 0538e28f..967d05e5 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -1586,22 +1586,22 @@ class Calculation ], 'LOGINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'LOGINV'], + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'], 'argumentCount' => '3', ], 'LOGNORMDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'LOGNORMDIST'], + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'cumulative'], 'argumentCount' => '3', ], 'LOGNORM.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'LOGNORMDIST2'], + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'distribution'], 'argumentCount' => '4', ], 'LOGNORM.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'LOGINV'], + 'functionCall' => [Statistical\Distributions\LogNormal::class, 'inverse'], 'argumentCount' => '3', ], 'LOOKUP' => [ @@ -1776,42 +1776,42 @@ class Calculation ], 'NORMDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMDIST'], + 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'], 'argumentCount' => '4', ], 'NORM.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMDIST'], + 'functionCall' => [Statistical\Distributions\Normal::class, 'distribution'], 'argumentCount' => '4', ], 'NORMINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMINV'], + 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'], 'argumentCount' => '3', ], 'NORM.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMINV'], + 'functionCall' => [Statistical\Distributions\Normal::class, 'inverse'], 'argumentCount' => '3', ], 'NORMSDIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMSDIST'], + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'cumulative'], 'argumentCount' => '1', ], 'NORM.S.DIST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMSDIST2'], + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'distribution'], 'argumentCount' => '1,2', ], 'NORMSINV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMSINV'], + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'], 'argumentCount' => '1', ], 'NORM.S.INV' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'NORMSINV'], + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'inverse'], 'argumentCount' => '1', ], 'NOT' => [ @@ -2651,12 +2651,12 @@ class Calculation ], 'ZTEST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'ZTEST'], + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'], 'argumentCount' => '2-3', ], 'Z.TEST' => [ 'category' => Category::CATEGORY_STATISTICAL, - 'functionCall' => [Statistical::class, 'ZTEST'], + 'functionCall' => [Statistical\Distributions\StandardNormal::class, 'zTest'], 'argumentCount' => '2-3', ], ]; diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 1576af98..3e90b21d 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -21,90 +21,6 @@ class Statistical const MAX_ITERATIONS = 256; const SQRT2PI = 2.5066282746310005024157652848110452530069867406099; - /* - * inverse_ncdf.php - * ------------------- - * begin : Friday, January 16, 2004 - * copyright : (C) 2004 Michael Nickerson - * email : nickersonm@yahoo.com - * - */ - private static function inverseNcdf($p) - { - // Inverse ncdf approximation by Peter J. Acklam, implementation adapted to - // PHP by Michael Nickerson, using Dr. Thomas Ziegler's C implementation as - // a guide. http://home.online.no/~pjacklam/notes/invnorm/index.html - // I have not checked the accuracy of this implementation. Be aware that PHP - // will truncate the coeficcients to 14 digits. - - // You have permission to use and distribute this function freely for - // whatever purpose you want, but please show common courtesy and give credit - // where credit is due. - - // Input paramater is $p - probability - where 0 < p < 1. - - // Coefficients in rational approximations - static $a = [ - 1 => -3.969683028665376e+01, - 2 => 2.209460984245205e+02, - 3 => -2.759285104469687e+02, - 4 => 1.383577518672690e+02, - 5 => -3.066479806614716e+01, - 6 => 2.506628277459239e+00, - ]; - - static $b = [ - 1 => -5.447609879822406e+01, - 2 => 1.615858368580409e+02, - 3 => -1.556989798598866e+02, - 4 => 6.680131188771972e+01, - 5 => -1.328068155288572e+01, - ]; - - static $c = [ - 1 => -7.784894002430293e-03, - 2 => -3.223964580411365e-01, - 3 => -2.400758277161838e+00, - 4 => -2.549732539343734e+00, - 5 => 4.374664141464968e+00, - 6 => 2.938163982698783e+00, - ]; - - static $d = [ - 1 => 7.784695709041462e-03, - 2 => 3.224671290700398e-01, - 3 => 2.445134137142996e+00, - 4 => 3.754408661907416e+00, - ]; - - // Define lower and upper region break-points. - $p_low = 0.02425; //Use lower region approx. below this - $p_high = 1 - $p_low; //Use upper region approx. above this - - if (0 < $p && $p < $p_low) { - // Rational approximation for lower region. - $q = sqrt(-2 * log($p)); - - return ((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) / - (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); - } elseif ($p_low <= $p && $p <= $p_high) { - // Rational approximation for central region. - $q = $p - 0.5; - $r = $q * $q; - - return ((((($a[1] * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $r + $a[6]) * $q / - ((((($b[1] * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + $b[5]) * $r + 1); - } elseif ($p_high < $p && $p < 1) { - // Rational approximation for upper region. - $q = sqrt(-2 * log(1 - $p)); - - return -((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) / - (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); - } - // If 0 < p < 1, return a null value - return Functions::NULL(); - } - /** * AVEDEV. * @@ -766,7 +682,7 @@ class Statistical return Functions::VALUE(); } - return self::NORMDIST($value, 0, 1, true) - 0.5; + return Statistical\Distributions\Normal::distribution($value, 0, 1, true) - 0.5; } /** @@ -1048,6 +964,11 @@ class Statistical * * Returns the inverse of the normal cumulative distribution * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\LogNormal::inverse() + * Use the inverse() method in the Statistical\Distributions\LogNormal class instead + * * @param float $probability * @param float $mean * @param float $stdDev @@ -1060,19 +981,7 @@ class Statistical */ public static function LOGINV($probability, $mean, $stdDev) { - $probability = Functions::flattenSingleValue($probability); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); - - if ((is_numeric($probability)) && (is_numeric($mean)) && (is_numeric($stdDev))) { - if (($probability < 0) || ($probability > 1) || ($stdDev <= 0)) { - return Functions::NAN(); - } - - return exp($mean + $stdDev * self::NORMSINV($probability)); - } - - return Functions::VALUE(); + return Statistical\Distributions\LogNormal::inverse($probability, $mean, $stdDev); } /** @@ -1081,6 +990,11 @@ class Statistical * Returns the cumulative lognormal distribution of x, where ln(x) is normally distributed * with parameters mean and standard_dev. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\LogNormal::cumulative() + * Use the cumulative() method in the Statistical\Distributions\LogNormal class instead + * * @param float $value * @param float $mean * @param float $stdDev @@ -1089,19 +1003,7 @@ class Statistical */ public static function LOGNORMDIST($value, $mean, $stdDev) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); - - if ((is_numeric($value)) && (is_numeric($mean)) && (is_numeric($stdDev))) { - if (($value <= 0) || ($stdDev <= 0)) { - return Functions::NAN(); - } - - return self::NORMSDIST((log($value) - $mean) / $stdDev); - } - - return Functions::VALUE(); + return Statistical\Distributions\LogNormal::cumulative($value, $mean, $stdDev); } /** @@ -1110,6 +1012,11 @@ class Statistical * Returns the lognormal distribution of x, where ln(x) is normally distributed * with parameters mean and standard_dev. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\LogNormal::distribution() + * Use the distribution() method in the Statistical\Distributions\LogNormal class instead + * * @param float $value * @param float $mean * @param float $stdDev @@ -1119,25 +1026,7 @@ class Statistical */ public static function LOGNORMDIST2($value, $mean, $stdDev, $cumulative = false) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); - $cumulative = (bool) Functions::flattenSingleValue($cumulative); - - if ((is_numeric($value)) && (is_numeric($mean)) && (is_numeric($stdDev))) { - if (($value <= 0) || ($stdDev <= 0)) { - return Functions::NAN(); - } - - if ($cumulative === true) { - return self::NORMSDIST2((log($value) - $mean) / $stdDev, true); - } - - return (1 / (sqrt(2 * M_PI) * $stdDev * $value)) * - exp(0 - ((log($value) - $mean) ** 2 / (2 * $stdDev ** 2))); - } - - return Functions::VALUE(); + return Statistical\Distributions\LogNormal::distribution($value, $mean, $stdDev, $cumulative); } /** @@ -1329,7 +1218,7 @@ class Statistical * * @Deprecated 1.18.0 * - * @see Statistical\Distributions\Binomial::negative::mode() + * @see Statistical\Distributions\Binomial::negative() * Use the negative() method in the Statistical\Distributions\Binomial class instead * * @param mixed (float) $failures Number of Failures @@ -1350,6 +1239,11 @@ class Statistical * function has a very wide range of applications in statistics, including hypothesis * testing. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Normal::distribution() + * Use the distribution() method in the Statistical\Distributions\Normal class instead + * * @param mixed (float) $value * @param mixed (float) $mean Mean Value * @param mixed (float) $stdDev Standard Deviation @@ -1359,24 +1253,7 @@ class Statistical */ public static function NORMDIST($value, $mean, $stdDev, $cumulative) { - $value = Functions::flattenSingleValue($value); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); - - if ((is_numeric($value)) && (is_numeric($mean)) && (is_numeric($stdDev))) { - if ($stdDev < 0) { - return Functions::NAN(); - } - if ((is_numeric($cumulative)) || (is_bool($cumulative))) { - if ($cumulative) { - return 0.5 * (1 + Engineering\Erf::erfValue(($value - $mean) / ($stdDev * sqrt(2)))); - } - - return (1 / (self::SQRT2PI * $stdDev)) * exp(0 - (($value - $mean) ** 2 / (2 * ($stdDev * $stdDev)))); - } - } - - return Functions::VALUE(); + return Statistical\Distributions\Normal::distribution($value, $mean, $stdDev, $cumulative); } /** @@ -1384,6 +1261,11 @@ class Statistical * * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\Normal::inverse() + * Use the inverse() method in the Statistical\Distributions\Normal class instead + * * @param mixed (float) $probability * @param mixed (float) $mean Mean Value * @param mixed (float) $stdDev Standard Deviation @@ -1392,22 +1274,7 @@ class Statistical */ public static function NORMINV($probability, $mean, $stdDev) { - $probability = Functions::flattenSingleValue($probability); - $mean = Functions::flattenSingleValue($mean); - $stdDev = Functions::flattenSingleValue($stdDev); - - if ((is_numeric($probability)) && (is_numeric($mean)) && (is_numeric($stdDev))) { - if (($probability < 0) || ($probability > 1)) { - return Functions::NAN(); - } - if ($stdDev < 0) { - return Functions::NAN(); - } - - return (self::inverseNcdf($probability) * $stdDev) + $mean; - } - - return Functions::VALUE(); + return Statistical\Distributions\Normal::inverse($probability, $mean, $stdDev); } /** @@ -1417,18 +1284,18 @@ class Statistical * a mean of 0 (zero) and a standard deviation of one. Use this function in place of a * table of standard normal curve areas. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\StandardNormal::cumulative() + * Use the cumulative() method in the Statistical\Distributions\StandardNormal class instead + * * @param mixed (float) $value * * @return float|string The result, or a string containing an error */ public static function NORMSDIST($value) { - $value = Functions::flattenSingleValue($value); - if (!is_numeric($value)) { - return Functions::VALUE(); - } - - return self::NORMDIST($value, 0, 1, true); + return Statistical\Distributions\StandardNormal::cumulative($value); } /** @@ -1438,6 +1305,11 @@ class Statistical * a mean of 0 (zero) and a standard deviation of one. Use this function in place of a * table of standard normal curve areas. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\StandardNormal::distribution() + * Use the distribution() method in the Statistical\Distributions\StandardNormal class instead + * * @param mixed (float) $value * @param mixed (bool) $cumulative * @@ -1445,13 +1317,7 @@ class Statistical */ public static function NORMSDIST2($value, $cumulative) { - $value = Functions::flattenSingleValue($value); - if (!is_numeric($value)) { - return Functions::VALUE(); - } - $cumulative = (bool) Functions::flattenSingleValue($cumulative); - - return self::NORMDIST($value, 0, 1, $cumulative); + return Statistical\Distributions\StandardNormal::distribution($value, $cumulative); } /** @@ -1459,13 +1325,18 @@ class Statistical * * Returns the inverse of the standard normal cumulative distribution * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\StandardNormal::inverse() + * Use the inverse() method in the Statistical\Distributions\StandardNormal class instead + * * @param mixed (float) $value * * @return float|string The result, or a string containing an error */ public static function NORMSINV($value) { - return self::NORMINV($value, 0, 1); + return Statistical\Distributions\StandardNormal::inverse($value); } /** @@ -2098,6 +1969,11 @@ class Statistical * For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be * greater than the average of observations in the data set (array) — that is, the observed sample mean. * + * @Deprecated 1.18.0 + * + * @see Statistical\Distributions\StandardNormal::zTest() + * Use the zTest() method in the Statistical\Distributions\StandardNormal class instead + * * @param float $dataSet * @param float $m0 Alpha Parameter * @param float $sigma Beta Parameter @@ -2106,15 +1982,6 @@ class Statistical */ public static function ZTEST($dataSet, $m0, $sigma = null) { - $dataSet = Functions::flattenArrayIndexed($dataSet); - $m0 = Functions::flattenSingleValue($m0); - $sigma = Functions::flattenSingleValue($sigma); - - if ($sigma === null) { - $sigma = StandardDeviations::STDEV($dataSet); - } - $n = count($dataSet); - - return 1 - self::NORMSDIST((Averages::average($dataSet) - $m0) / ($sigma / sqrt($n))); + return Statistical\Distributions\StandardNormal::zTest($dataSet, $m0, $sigma); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php index c9ef0f16..40adc9e3 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php @@ -4,7 +4,6 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical; use PhpOffice\PhpSpreadsheet\Calculation\Exception; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\Statistical; class Confidence { @@ -39,6 +38,6 @@ class Confidence return Functions::NAN(); } - return Statistical::NORMSINV(1 - $alpha / 2) * $stdDev / sqrt($size); + return Distributions\StandardNormal::inverse(1 - $alpha / 2) * $stdDev / sqrt($size); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php new file mode 100644 index 00000000..87b464fa --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php @@ -0,0 +1,121 @@ +getMessage(); + } + + if (($value <= 0) || ($stdDev <= 0)) { + return Functions::NAN(); + } + + return StandardNormal::cumulative((log($value) - $mean) / $stdDev); + } + + /** + * LOGNORM.DIST. + * + * Returns the lognormal distribution of x, where ln(x) is normally distributed + * with parameters mean and standard_dev. + * + * @param mixed (float) $value + * @param mixed (float) $mean + * @param mixed (float) $stdDev + * @param mixed (bool) $cumulative + * + * @return float|string The result, or a string containing an error + */ + 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); + + try { + $value = self::validateFloat($value); + $mean = self::validateFloat($mean); + $stdDev = self::validateFloat($stdDev); + $cumulative = self::validateBool($cumulative); + } catch (Exception $e) { + return $e->getMessage(); + } + + if (($value <= 0) || ($stdDev <= 0)) { + return Functions::NAN(); + } + + if ($cumulative === true) { + return StandardNormal::distribution((log($value) - $mean) / $stdDev, true); + } + + return (1 / (sqrt(2 * M_PI) * $stdDev * $value)) * + exp(0 - ((log($value) - $mean) ** 2 / (2 * $stdDev ** 2))); + } + + /** + * LOGINV. + * + * Returns the inverse of the normal cumulative distribution + * + * @param mixed (float) $probability + * @param mixed (float) $mean + * @param mixed (float) $stdDev + * + * @return 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 + * (as described at) http://home.online.no/~pjacklam/notes/invnorm/ + */ + public static function inverse($probability, $mean, $stdDev) + { + $probability = Functions::flattenSingleValue($probability); + $mean = Functions::flattenSingleValue($mean); + $stdDev = Functions::flattenSingleValue($stdDev); + + try { + $probability = self::validateProbability($probability); + $mean = self::validateFloat($mean); + $stdDev = self::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev <= 0) { + return Functions::NAN(); + } + + return exp($mean + $stdDev * StandardNormal::inverse($probability)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php new file mode 100644 index 00000000..b0c5552a --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php @@ -0,0 +1,168 @@ +getMessage(); + } + + if ($stdDev < 0) { + return Functions::NAN(); + } + + if ($cumulative) { + return 0.5 * (1 + Engineering\Erf::erfValue(($value - $mean) / ($stdDev * sqrt(2)))); + } + + return (1 / (self::SQRT2PI * $stdDev)) * exp(0 - (($value - $mean) ** 2 / (2 * ($stdDev * $stdDev)))); + } + + /** + * NORMINV. + * + * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. + * + * @param mixed (float) $probability + * @param mixed (float) $mean Mean Value + * @param mixed (float) $stdDev Standard Deviation + * + * @return float|string The result, or a string containing an error + */ + public static function inverse($probability, $mean, $stdDev) + { + $probability = Functions::flattenSingleValue($probability); + $mean = Functions::flattenSingleValue($mean); + $stdDev = Functions::flattenSingleValue($stdDev); + + try { + $probability = self::validateProbability($probability); + $mean = self::validateFloat($mean); + $stdDev = self::validateFloat($stdDev); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($stdDev < 0) { + return Functions::NAN(); + } + + return (self::inverseNcdf($probability) * $stdDev) + $mean; + } + + /* + * inverse_ncdf.php + * ------------------- + * begin : Friday, January 16, 2004 + * copyright : (C) 2004 Michael Nickerson + * email : nickersonm@yahoo.com + * + */ + private static function inverseNcdf($p) + { + // Inverse ncdf approximation by Peter J. Acklam, implementation adapted to + // PHP by Michael Nickerson, using Dr. Thomas Ziegler's C implementation as + // a guide. http://home.online.no/~pjacklam/notes/invnorm/index.html + // I have not checked the accuracy of this implementation. Be aware that PHP + // will truncate the coeficcients to 14 digits. + + // You have permission to use and distribute this function freely for + // whatever purpose you want, but please show common courtesy and give credit + // where credit is due. + + // Input paramater is $p - probability - where 0 < p < 1. + + // Coefficients in rational approximations + static $a = [ + 1 => -3.969683028665376e+01, + 2 => 2.209460984245205e+02, + 3 => -2.759285104469687e+02, + 4 => 1.383577518672690e+02, + 5 => -3.066479806614716e+01, + 6 => 2.506628277459239e+00, + ]; + + static $b = [ + 1 => -5.447609879822406e+01, + 2 => 1.615858368580409e+02, + 3 => -1.556989798598866e+02, + 4 => 6.680131188771972e+01, + 5 => -1.328068155288572e+01, + ]; + + static $c = [ + 1 => -7.784894002430293e-03, + 2 => -3.223964580411365e-01, + 3 => -2.400758277161838e+00, + 4 => -2.549732539343734e+00, + 5 => 4.374664141464968e+00, + 6 => 2.938163982698783e+00, + ]; + + static $d = [ + 1 => 7.784695709041462e-03, + 2 => 3.224671290700398e-01, + 3 => 2.445134137142996e+00, + 4 => 3.754408661907416e+00, + ]; + + // Define lower and upper region break-points. + $p_low = 0.02425; //Use lower region approx. below this + $p_high = 1 - $p_low; //Use upper region approx. above this + + if (0 < $p && $p < $p_low) { + // Rational approximation for lower region. + $q = sqrt(-2 * log($p)); + + return ((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) / + (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); + } elseif ($p_high < $p && $p < 1) { + // Rational approximation for upper region. + $q = sqrt(-2 * log(1 - $p)); + + return -((((($c[1] * $q + $c[2]) * $q + $c[3]) * $q + $c[4]) * $q + $c[5]) * $q + $c[6]) / + (((($d[1] * $q + $d[2]) * $q + $d[3]) * $q + $d[4]) * $q + 1); + } + + // Rational approximation for central region. + $q = $p - 0.5; + $r = $q * $q; + + return ((((($a[1] * $r + $a[2]) * $r + $a[3]) * $r + $a[4]) * $r + $a[5]) * $r + $a[6]) * $q / + ((((($b[1] * $r + $b[2]) * $r + $b[3]) * $r + $b[4]) * $r + $b[5]) * $r + 1); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php new file mode 100644 index 00000000..c3049f6d --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php @@ -0,0 +1,89 @@ + Date: Sun, 28 Mar 2021 20:35:40 +0900 Subject: [PATCH 80/89] Introduce PHPStan To improve the feedback loop on code quality with a process that can be run locally by the developers, instead of only on Scrutinizer. --- .github/workflows/main.yml | 31 +++++ composer.json | 4 +- composer.lock | 62 +++++++++- phpstan.neon.dist | 11 ++ .../Calculation/Calculation.php | 27 +++-- src/PhpSpreadsheet/Calculation/DateTime.php | 4 +- src/PhpSpreadsheet/Calculation/Financial.php | 2 + src/PhpSpreadsheet/Chart/Renderer/JpGraph.php | 4 + src/PhpSpreadsheet/IOFactory.php | 2 +- src/PhpSpreadsheet/Reader/Xls.php | 67 ++++++----- src/PhpSpreadsheet/Reader/Xls/Color.php | 2 +- src/PhpSpreadsheet/Reader/Xlsx.php | 10 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 2 +- src/PhpSpreadsheet/Reader/Xml.php | 1 + src/PhpSpreadsheet/Shared/Drawing.php | 2 + src/PhpSpreadsheet/Shared/Font.php | 1 + .../Shared/JAMA/EigenvalueDecomposition.php | 15 ++- .../Shared/JAMA/LUDecomposition.php | 2 + .../JAMA/SingularValueDecomposition.php | 1 + src/PhpSpreadsheet/Shared/OLE.php | 5 +- src/PhpSpreadsheet/Shared/Trend/Trend.php | 2 + src/PhpSpreadsheet/Spreadsheet.php | 7 +- src/PhpSpreadsheet/Style/Border.php | 5 +- src/PhpSpreadsheet/Style/Borders.php | 15 +-- .../Style/NumberFormat/NumberFormatter.php | 1 + src/PhpSpreadsheet/Style/Style.php | 106 +++++++++--------- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 3 + src/PhpSpreadsheet/Writer/Xls.php | 1 + src/PhpSpreadsheet/Writer/Xls/Parser.php | 6 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 79 +++++-------- src/PhpSpreadsheet/Writer/Xls/Xf.php | 87 ++++++++------ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 5 +- .../Functions/DateTime/WeekDayTest.php | 8 +- .../Reader/Security/XmlScannerTest.php | 2 +- 34 files changed, 370 insertions(+), 212 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ce38f646..87d933e2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -124,6 +124,37 @@ jobs: - name: Code style with PHP_CodeSniffer run: ./vendor/bin/phpcs -q --report=checkstyle | cs2pr + phpstan: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP, with composer and extensions + uses: shivammathur/setup-php@v2 + with: + php-version: 7.4 + extensions: ctype, dom, gd, iconv, fileinfo, libxml, mbstring, simplexml, xml, xmlreader, xmlwriter, zip, zlib + coverage: none + tools: cs2pr + + - name: Get composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + + - name: Cache composer dependencies + uses: actions/cache@v2 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + + - name: Static analysis with PHPStan + run: ./vendor/bin/phpstan analyse + coverage: runs-on: ubuntu-latest steps: diff --git a/composer.json b/composer.json index 3b4ed556..d0c3a16d 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ "check": [ "php-cs-fixer fix --ansi --dry-run --diff", "phpcs", - "phpunit --color=always" + "phpunit --color=always", + "phpstan analyse --ansi" ], "fix": [ "php-cs-fixer fix --ansi" @@ -79,6 +80,7 @@ "jpgraph/jpgraph": "^4.0", "mpdf/mpdf": "^8.0", "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^0.12.82", "phpunit/phpunit": "^8.5", "squizlabs/php_codesniffer": "^3.5", "tecnickcom/tcpdf": "^6.3" diff --git a/composer.lock b/composer.lock index 3f6efb00..e4060972 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "6c8f34baf3385a533fade30a9a9ad6f1", + "content-hash": "89b62d75519340c289a3a763245f1ca0", "packages": [ { "name": "ezyang/htmlpurifier", @@ -1963,6 +1963,66 @@ }, "time": "2021-03-17T13:42:18+00:00" }, + { + "name": "phpstan/phpstan", + "version": "0.12.82", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpstan.git", + "reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3920f0fb0aff39263d3a4cb0bca120a67a1a6a11", + "reference": "3920f0fb0aff39263d3a4cb0bca120a67a1a6a11", + "shasum": "" + }, + "require": { + "php": "^7.1|^8.0" + }, + "conflict": { + "phpstan/phpstan-shim": "*" + }, + "bin": [ + "phpstan", + "phpstan.phar" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12-dev" + } + }, + "autoload": { + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPStan - PHP Static Analysis Tool", + "support": { + "issues": "https://github.com/phpstan/phpstan/issues", + "source": "https://github.com/phpstan/phpstan/tree/0.12.82" + }, + "funding": [ + { + "url": "https://github.com/ondrejmirtes", + "type": "github" + }, + { + "url": "https://www.patreon.com/phpstan", + "type": "patreon" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan", + "type": "tidelift" + } + ], + "time": "2021-03-19T06:08:17+00:00" + }, { "name": "phpunit/php-code-coverage", "version": "7.0.14", diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 00000000..476513dc --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,11 @@ +parameters: + level: 1 + paths: + - src/ + - tests/ + ignoreErrors: + - '~^Class GdImage not found\.$~' + + # Ignore all JpGraph issues + - '~^Constant (MARK_CIRCLE|MARK_CROSS|MARK_DIAMOND|MARK_DTRIANGLE|MARK_FILLEDCIRCLE|MARK_SQUARE|MARK_STAR|MARK_UTRIANGLE|MARK_X|SIDE_RIGHT) not found\.$~' + - '~^Instantiated class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot) not found\.$~' diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 967d05e5..0aa2a6da 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -3154,6 +3154,7 @@ class Calculation // Return Excel errors "as is" return $value; } + // Return strings wrapped in quotes return self::FORMULA_STRING_QUOTE . $value . self::FORMULA_STRING_QUOTE; } elseif ((is_float($value)) && ((is_nan($value)) || (is_infinite($value)))) { @@ -3794,13 +3795,13 @@ class Calculation $pCellParent = ($pCell !== null) ? $pCell->getWorksheet() : null; $regexpMatchString = '/^(' . self::CALCULATION_REGEXP_FUNCTION . - '|' . self::CALCULATION_REGEXP_CELLREF . - '|' . self::CALCULATION_REGEXP_NUMBER . - '|' . self::CALCULATION_REGEXP_STRING . - '|' . self::CALCULATION_REGEXP_OPENBRACE . - '|' . self::CALCULATION_REGEXP_DEFINEDNAME . - '|' . self::CALCULATION_REGEXP_ERROR . - ')/sui'; + '|' . self::CALCULATION_REGEXP_CELLREF . + '|' . self::CALCULATION_REGEXP_NUMBER . + '|' . self::CALCULATION_REGEXP_STRING . + '|' . self::CALCULATION_REGEXP_OPENBRACE . + '|' . self::CALCULATION_REGEXP_DEFINEDNAME . + '|' . self::CALCULATION_REGEXP_ERROR . + ')/sui'; // Start with initialisation $index = 0; @@ -3939,6 +3940,7 @@ class Calculation } // Check the argument count $argumentCountError = false; + $expectedArgumentCountString = null; if (is_numeric($expectedArgumentCount)) { if ($expectedArgumentCount < 0) { if ($argumentCount > abs($expectedArgumentCount)) { @@ -4203,7 +4205,7 @@ class Calculation ((preg_match('/^' . self::CALCULATION_REGEXP_CELLREF . '.*/Ui', substr($formula, $index), $match)) && ($output[count($output) - 1]['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[count($output) - 1]['type'] == 'Defined Name' || $output[count($output) - 1]['type'] == 'Value') ) ) { while ( @@ -4645,6 +4647,9 @@ class Calculation $this->debugLog->writeDebugLog('Evaluating Function ', self::localeFunc($functionName), '() with ', (($argCount == 0) ? 'no' : $argCount), ' argument', (($argCount == 1) ? '' : 's')); } if ((isset(self::$phpSpreadsheetFunctions[$functionName])) || (isset(self::$controlFunctions[$functionName]))) { // function + $passByReference = false; + $passCellReference = false; + $functionCall = null; if (isset(self::$phpSpreadsheetFunctions[$functionName])) { $functionCall = self::$phpSpreadsheetFunctions[$functionName]['functionCall']; $passByReference = isset(self::$phpSpreadsheetFunctions[$functionName]['passByReference']); @@ -4945,6 +4950,9 @@ class Calculation } break; + + default: + throw new Exception('Unsupported binary comparison operation'); } // Log the result details @@ -5062,6 +5070,9 @@ class Calculation $result = $operand1 ** $operand2; break; + + default: + throw new Exception('Unsupported numeric binary operation'); } } } diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index 744d9589..e3580cde 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -651,7 +651,7 @@ class DateTime * * Returns the ISO 8601 week number of the year for a specified date. * - * @Deprecated 2.0.0 Use the funcIsoWeeknum method in the DateTimeExcel\Isoweeknum class instead + * @Deprecated 2.0.0 Use the funcIsoWeeknum method in the DateTimeExcel\IsoWeekNum class instead * * Excel Function: * ISOWEEKNUM(dateValue) @@ -663,7 +663,7 @@ class DateTime */ public static function ISOWEEKNUM($dateValue = 1) { - return DateTimeExcel\IsoweekNum::funcIsoWeekNum($dateValue); + return DateTimeExcel\IsoWeekNum::funcIsoWeekNum($dateValue); } /** diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 11bbf7a6..547ad6b3 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -20,6 +20,8 @@ class Financial { $pmt = self::PMT($rate, $nper, $pv, $fv, $type); $capital = $pv; + $interest = 0; + $principal = 0; for ($i = 1; $i <= $per; ++$i) { $interest = ($type && $i == 1) ? 0 : -$capital * $rate; $principal = $pmt - $interest; diff --git a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php index 02fbfed7..0ab70870 100644 --- a/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php +++ b/src/PhpSpreadsheet/Chart/Renderer/JpGraph.php @@ -301,6 +301,8 @@ class JpGraph implements IRenderer $seriesPlots = []; if ($grouping == 'percentStacked') { $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; } // Loop through each data series in turn @@ -376,6 +378,8 @@ class JpGraph implements IRenderer $seriesPlots = []; if ($grouping == 'percentStacked') { $sumValues = $this->percentageSumCalculation($groupID, $seriesCount); + } else { + $sumValues = []; } // Loop through each data series in turn diff --git a/src/PhpSpreadsheet/IOFactory.php b/src/PhpSpreadsheet/IOFactory.php index ab04e969..06006edc 100644 --- a/src/PhpSpreadsheet/IOFactory.php +++ b/src/PhpSpreadsheet/IOFactory.php @@ -120,7 +120,7 @@ abstract class IOFactory $reader = self::createReader($guessedReader); // Let's see if we are lucky - if (isset($reader) && $reader->canRead($filename)) { + if ($reader->canRead($filename)) { return $reader; } } diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index 6d6b87fd..faa047da 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -801,9 +801,10 @@ class Xls extends BaseReader } // treat MSODRAWINGGROUP records, workbook-level Escher + $escherWorkbook = null; if (!$this->readDataOnly && $this->drawingGroupData) { - $escherWorkbook = new Escher(); - $reader = new Xls\Escher($escherWorkbook); + $escher = new Escher(); + $reader = new Xls\Escher($escher); $escherWorkbook = $reader->load($this->drawingGroupData); } @@ -1133,38 +1134,40 @@ class Xls extends BaseReader continue 2; } - $BSECollection = $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection(); - $BSE = $BSECollection[$BSEindex - 1]; - $blipType = $BSE->getBlipType(); + if ($escherWorkbook) { + $BSECollection = $escherWorkbook->getDggContainer()->getBstoreContainer()->getBSECollection(); + $BSE = $BSECollection[$BSEindex - 1]; + $blipType = $BSE->getBlipType(); - // need check because some blip types are not supported by Escher reader such as EMF - if ($blip = $BSE->getBlip()) { - $ih = imagecreatefromstring($blip->getData()); - $drawing = new MemoryDrawing(); - $drawing->setImageResource($ih); + // need check because some blip types are not supported by Escher reader such as EMF + if ($blip = $BSE->getBlip()) { + $ih = imagecreatefromstring($blip->getData()); + $drawing = new MemoryDrawing(); + $drawing->setImageResource($ih); - // width, height, offsetX, offsetY - $drawing->setResizeProportional(false); - $drawing->setWidth($width); - $drawing->setHeight($height); - $drawing->setOffsetX($offsetX); - $drawing->setOffsetY($offsetY); + // width, height, offsetX, offsetY + $drawing->setResizeProportional(false); + $drawing->setWidth($width); + $drawing->setHeight($height); + $drawing->setOffsetX($offsetX); + $drawing->setOffsetY($offsetY); - switch ($blipType) { - case BSE::BLIPTYPE_JPEG: - $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG); - $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG); + switch ($blipType) { + case BSE::BLIPTYPE_JPEG: + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_JPEG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_JPEG); - break; - case BSE::BLIPTYPE_PNG: - $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG); - $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG); + break; + case BSE::BLIPTYPE_PNG: + $drawing->setRenderingFunction(MemoryDrawing::RENDERING_PNG); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_PNG); - break; + break; + } + + $drawing->setWorksheet($this->phpSheet); + $drawing->setCoordinates($spContainer->getStartCoordinates()); } - - $drawing->setWorksheet($this->phpSheet); - $drawing->setCoordinates($spContainer->getStartCoordinates()); } break; @@ -2742,6 +2745,7 @@ class Xls extends BaseReader $sheetType = ord($recordData[5]); // offset: 6; size: var; sheet name + $rec_name = null; if ($this->version == self::XLS_BIFF8) { $string = self::readUnicodeStringShort(substr($recordData, 6)); $rec_name = $string['value']; @@ -3018,12 +3022,14 @@ class Xls extends BaseReader // bit: 3; mask: 0x03; 0 = ordinary; 1 = Rich-Text $hasRichText = (($optionFlags & 0x08) != 0); + $formattingRuns = 0; if ($hasRichText) { // number of Rich-Text formatting runs $formattingRuns = self::getUInt2d($recordData, $pos); $pos += 2; } + $extendedRunLength = 0; if ($hasAsian) { // size of Asian phonetic setting $extendedRunLength = self::getInt4d($recordData, $pos); @@ -3034,6 +3040,7 @@ class Xls extends BaseReader $len = ($isCompressed) ? $numChars : $numChars * 2; // look up limit position - Check it again to be sure that no error occurs when parsing SST structure + $limitpos = null; foreach ($spliceOffsets as $spliceOffset) { // it can happen that the string is empty, therefore we need // <= and not just < @@ -4385,6 +4392,8 @@ class Xls extends BaseReader // offset: 4; size: 2; index to first visible colum $firstVisibleColumn = self::getUInt2d($recordData, 4); + $zoomscaleInPageBreakPreview = 0; + $zoomscaleInNormalView = 0; if ($this->version === self::XLS_BIFF8) { // offset: 8; size: 2; not used // offset: 10; size: 2; cached magnification factor in page break preview (in percent); 0 = Default (60%) @@ -7636,6 +7645,8 @@ class Xls extends BaseReader $size = 9; break; + default: + throw new PhpSpreadsheetException('Unsupported BIFF8 constant'); } return [ diff --git a/src/PhpSpreadsheet/Reader/Xls/Color.php b/src/PhpSpreadsheet/Reader/Xls/Color.php index c45f88c7..06c2d0b9 100644 --- a/src/PhpSpreadsheet/Reader/Xls/Color.php +++ b/src/PhpSpreadsheet/Reader/Xls/Color.php @@ -20,7 +20,7 @@ class Color if ($color <= 0x07 || $color >= 0x40) { // special built-in color return Color\BuiltIn::lookup($color); - } elseif (isset($palette, $palette[$color - 8])) { + } elseif (isset($palette[$color - 8])) { // palette color, color index 0x08 maps to pallete index 0 return $palette[$color - 8]; } diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 219a49fb..e47ad7b0 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -430,7 +430,7 @@ class Xlsx extends BaseReader 'SimpleXMLElement', Settings::getLibXmlLoaderOptions() ); - if (isset($xmlStrings, $xmlStrings->si)) { + if (isset($xmlStrings->si)) { foreach ($xmlStrings->si as $val) { if (isset($val->t)) { $sharedStrings[] = StringHelper::controlCharacterOOXML2PHP((string) $val->t); @@ -511,10 +511,7 @@ class Xlsx extends BaseReader $numFmt = NumberFormat::builtInFormatCode((int) $xf['numFmtId']); } } - $quotePrefix = false; - if (isset($xf['quotePrefix'])) { - $quotePrefix = (bool) $xf['quotePrefix']; - } + $quotePrefix = (bool) ($xf['quotePrefix'] ?? false); $style = (object) [ 'numFmt' => $numFmt ?? NumberFormat::FORMAT_GENERAL, @@ -544,6 +541,8 @@ class Xlsx extends BaseReader } } + $quotePrefix = (bool) ($xf['quotePrefix'] ?? false); + $cellStyle = (object) [ 'numFmt' => $numFmt, 'font' => $xmlStyles->fonts->font[(int) ($xf['fontId'])], @@ -1081,6 +1080,7 @@ class Xlsx extends BaseReader } if ($xmlSheet->drawing && !$this->readDataOnly) { $unparsedDrawings = []; + $fileDrawing = null; foreach ($xmlSheet->drawing as $drawing) { $drawingRelId = (string) self::getArrayItem($drawing->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'id'); $fileDrawing = $drawings[$drawingRelId]; diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 5a3439f2..5e86c60a 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -61,7 +61,7 @@ class Chart $XaxisLabel = $YaxisLabel = $legend = $title = null; $dispBlanksAs = $plotVisOnly = null; - + $plotArea = null; foreach ($chartElementsC as $chartElementKey => $chartElement) { switch ($chartElementKey) { case 'chart': diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index a827d17a..58d38b0d 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -436,6 +436,7 @@ class Xml extends BaseReader // Create new Worksheet $spreadsheet->createSheet(); $spreadsheet->setActiveSheetIndex($worksheetID); + $worksheetName = ''; if (isset($worksheet_ss['Name'])) { $worksheetName = (string) $worksheet_ss['Name']; // Use false for $updateFormulaCellReferences to prevent adjustment of worksheet references in diff --git a/src/PhpSpreadsheet/Shared/Drawing.php b/src/PhpSpreadsheet/Shared/Drawing.php index f41fb695..67c015c4 100644 --- a/src/PhpSpreadsheet/Shared/Drawing.php +++ b/src/PhpSpreadsheet/Shared/Drawing.php @@ -171,6 +171,8 @@ class Drawing // Process the header // Structure: http://www.fastgraph.com/help/bmp_header_format.html + $width = 0; + $height = 0; if (substr($header, 0, 4) == '424d') { // Cut it in parts of 2 bytes $header_parts = str_split($header, 2); diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 162e9730..4061b370 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -244,6 +244,7 @@ class Font // Try to get the exact text width in pixels $approximate = self::$autoSizeMethod == self::AUTOSIZE_METHOD_APPROX; + $columnWidth = 0; if (!$approximate) { $columnWidthAdjust = ceil(self::getTextWidthPixelsExact('n', $font, 0) * 1.07); diff --git a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php index 4c67c3a9..5c6ccfd3 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/EigenvalueDecomposition.php @@ -18,9 +18,9 @@ namespace PhpOffice\PhpSpreadsheet\Shared\JAMA; * conditioned, or even singular, so the validity of the equation * A = V*D*inverse(V) depends upon V.cond(). * - * @author Paul Meagher + * @author Paul Meagher * - * @version 1.1 + * @version 1.1 */ class EigenvalueDecomposition { @@ -70,6 +70,11 @@ class EigenvalueDecomposition private $cdivi; + /** + * @var array + */ + private $A; + /** * Symmetric Householder reduction to tridiagonal form. */ @@ -80,6 +85,7 @@ class EigenvalueDecomposition // Auto. Comp., Vol.ii-Linear Algebra, and the corresponding // Fortran subroutine in EISPACK. $this->d = $this->V[$this->n - 1]; + $j = 0; // Householder reduction to tridiagonal form. for ($i = $this->n - 1; $i > 0; --$i) { $i_ = $i - 1; @@ -781,9 +787,9 @@ class EigenvalueDecomposition /** * Constructor: Check for symmetry, then construct the eigenvalue decomposition. * - * @param mixed $Arg A Square matrix + * @param Matrix $Arg A Square matrix */ - public function __construct($Arg) + public function __construct(Matrix $Arg) { $this->A = $Arg->getArray(); $this->n = $Arg->getColumnDimension(); @@ -848,6 +854,7 @@ class EigenvalueDecomposition */ public function getD() { + $D = []; for ($i = 0; $i < $this->n; ++$i) { $D[$i] = array_fill(0, $this->n, 0.0); $D[$i][$i] = $this->d[$i]; diff --git a/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php index 6db17c28..e16d6a21 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/LUDecomposition.php @@ -135,6 +135,7 @@ class LUDecomposition */ public function getL() { + $L = []; for ($i = 0; $i < $this->m; ++$i) { for ($j = 0; $j < $this->n; ++$j) { if ($i > $j) { @@ -159,6 +160,7 @@ class LUDecomposition */ public function getU() { + $U = []; for ($i = 0; $i < $this->n; ++$i) { for ($j = 0; $j < $this->n; ++$j) { if ($i <= $j) { diff --git a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php index b997fb7c..afd9ed0f 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php @@ -476,6 +476,7 @@ class SingularValueDecomposition */ public function getS() { + $S = []; for ($i = 0; $i < $this->n; ++$i) { for ($j = 0; $j < $this->n; ++$j) { $S[$i][$j] = 0.0; diff --git a/src/PhpSpreadsheet/Shared/OLE.php b/src/PhpSpreadsheet/Shared/OLE.php index 1c745bdb..f65fbca7 100644 --- a/src/PhpSpreadsheet/Shared/OLE.php +++ b/src/PhpSpreadsheet/Shared/OLE.php @@ -21,6 +21,7 @@ namespace PhpOffice\PhpSpreadsheet\Shared; // +----------------------------------------------------------------------+ // +use PhpOffice\PhpSpreadsheet\Exception; use PhpOffice\PhpSpreadsheet\Reader\Exception as ReaderException; use PhpOffice\PhpSpreadsheet\Shared\OLE\ChainedBlockStream; use PhpOffice\PhpSpreadsheet\Shared\OLE\PPS\Root; @@ -317,7 +318,7 @@ class OLE break; default: - break; + throw new Exception('Unsupported PPS type'); } fseek($fh, 1, SEEK_CUR); $pps->Type = $type; @@ -496,7 +497,7 @@ class OLE */ public static function localDateToOLE($date) { - if (!isset($date)) { + if (!$date) { return "\x00\x00\x00\x00\x00\x00\x00\x00"; } diff --git a/src/PhpSpreadsheet/Shared/Trend/Trend.php b/src/PhpSpreadsheet/Shared/Trend/Trend.php index d0a117cb..24570d59 100644 --- a/src/PhpSpreadsheet/Shared/Trend/Trend.php +++ b/src/PhpSpreadsheet/Shared/Trend/Trend.php @@ -91,6 +91,8 @@ class Trend case self::TREND_BEST_FIT_NO_POLY: // If the request is to determine the best fit regression, then we test each Trend line in turn // Start by generating an instance of each available Trend method + $bestFit = []; + $bestFitValue = []; foreach (self::$trendTypes as $trendMethod) { $className = '\PhpOffice\PhpSpreadsheet\Shared\Trend\\' . $trendType . 'BestFit'; $bestFit[$trendMethod] = new $className($yValues, $xValues, $const); diff --git a/src/PhpSpreadsheet/Spreadsheet.php b/src/PhpSpreadsheet/Spreadsheet.php index 51f558a1..59304804 100644 --- a/src/PhpSpreadsheet/Spreadsheet.php +++ b/src/PhpSpreadsheet/Spreadsheet.php @@ -55,7 +55,7 @@ class Spreadsheet /** * Calculation Engine. * - * @var Calculation + * @var null|Calculation */ private $calculationEngine; @@ -505,8 +505,8 @@ class Spreadsheet */ public function __destruct() { - $this->calculationEngine = null; $this->disconnectWorksheets(); + $this->calculationEngine = null; } /** @@ -527,7 +527,7 @@ class Spreadsheet /** * Return the calculation engine for this worksheet. * - * @return Calculation + * @return null|Calculation */ public function getCalculationEngine() { @@ -1343,6 +1343,7 @@ class Spreadsheet // remove cellXfs without references and create mapping so we can update xfIndex // for all cells and columns $countNeededCellXfs = 0; + $map = []; foreach ($this->cellXfCollection as $index => $cellXf) { if ($countReferencesCellXf[$index] > 0 || $index == 0) { // we must never remove the first cellXf ++$countNeededCellXfs; diff --git a/src/PhpSpreadsheet/Style/Border.php b/src/PhpSpreadsheet/Style/Border.php index 1d3096f0..d11fa0ca 100644 --- a/src/PhpSpreadsheet/Style/Border.php +++ b/src/PhpSpreadsheet/Style/Border.php @@ -47,11 +47,8 @@ class Border extends Supervisor * @param bool $isSupervisor Flag indicating if this is a supervisor or not * Leave this value at default unless you understand exactly what * its ramifications are - * @param bool $isConditional Flag indicating if this is a conditional style or not - * Leave this value at default unless you understand exactly what - * its ramifications are */ - public function __construct($isSupervisor = false, $isConditional = false) + public function __construct($isSupervisor = false) { // Supervisor? parent::__construct($isSupervisor); diff --git a/src/PhpSpreadsheet/Style/Borders.php b/src/PhpSpreadsheet/Style/Borders.php index a1acfdd4..eeb4932a 100644 --- a/src/PhpSpreadsheet/Style/Borders.php +++ b/src/PhpSpreadsheet/Style/Borders.php @@ -95,21 +95,18 @@ class Borders extends Supervisor * @param bool $isSupervisor Flag indicating if this is a supervisor or not * Leave this value at default unless you understand exactly what * its ramifications are - * @param bool $isConditional Flag indicating if this is a conditional style or not - * Leave this value at default unless you understand exactly what - * its ramifications are */ - public function __construct($isSupervisor = false, $isConditional = false) + public function __construct($isSupervisor = false) { // Supervisor? parent::__construct($isSupervisor); // Initialise values - $this->left = new Border($isSupervisor, $isConditional); - $this->right = new Border($isSupervisor, $isConditional); - $this->top = new Border($isSupervisor, $isConditional); - $this->bottom = new Border($isSupervisor, $isConditional); - $this->diagonal = new Border($isSupervisor, $isConditional); + $this->left = new Border($isSupervisor); + $this->right = new Border($isSupervisor); + $this->top = new Border($isSupervisor); + $this->bottom = new Border($isSupervisor); + $this->diagonal = new Border($isSupervisor); $this->diagonalDirection = self::DIAGONAL_NONE; // Specially for supervisor diff --git a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php index 36cdcf03..9a4f32ac 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/NumberFormatter.php @@ -35,6 +35,7 @@ class NumberFormatter if ($maskingBlockCount > 1) { $maskingBlocks = array_reverse($maskingBlocks[0]); + $offset = 0; foreach ($maskingBlocks as $block) { $size = strlen($block[0]); $divisor = 10 ** $size; diff --git a/src/PhpSpreadsheet/Style/Style.php b/src/PhpSpreadsheet/Style/Style.php index 7fec9a00..d3653ed5 100644 --- a/src/PhpSpreadsheet/Style/Style.php +++ b/src/PhpSpreadsheet/Style/Style.php @@ -80,7 +80,7 @@ class Style extends Supervisor // Initialise values $this->font = new Font($isSupervisor, $isConditional); $this->fill = new Fill($isSupervisor, $isConditional); - $this->borders = new Borders($isSupervisor, $isConditional); + $this->borders = new Borders($isSupervisor); $this->alignment = new Alignment($isSupervisor, $isConditional); $this->numberFormat = new NumberFormat($isSupervisor, $isConditional); $this->protection = new Protection($isSupervisor, $isConditional); @@ -257,11 +257,11 @@ class Style extends Supervisor // start column index for region $colStart = ($x == 3) ? Coordinate::stringFromColumnIndex($rangeEnd[0]) - : Coordinate::stringFromColumnIndex($rangeStart[0] + $x - 1); + : Coordinate::stringFromColumnIndex($rangeStart[0] + $x - 1); // end column index for region $colEnd = ($x == 1) ? Coordinate::stringFromColumnIndex($rangeStart[0]) - : Coordinate::stringFromColumnIndex($rangeEnd[0] - $xMax + $x); + : Coordinate::stringFromColumnIndex($rangeEnd[0] - $xMax + $x); for ($y = 1; $y <= $yMax; ++$y) { // which edges are touching the region @@ -349,56 +349,11 @@ class Style extends Supervisor } // First loop through columns, rows, or cells to find out which styles are affected by this operation - switch ($selectionType) { - case 'COLUMN': - $oldXfIndexes = []; - for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { - $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; - } - foreach ($this->getActiveSheet()->getColumnIterator($rangeStart0, $rangeEnd0) as $columnIterator) { - $cellIterator = $columnIterator->getCellIterator(); - $cellIterator->setIterateOnlyExistingCells(true); - foreach ($cellIterator as $columnCell) { - if ($columnCell !== null) { - $columnCell->getStyle()->applyFromArray($pStyles); - } - } - } - - break; - case 'ROW': - $oldXfIndexes = []; - for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { - if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() === null) { - $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style - } else { - $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true; - } - } - foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) { - $cellIterator = $rowIterator->getCellIterator(); - $cellIterator->setIterateOnlyExistingCells(true); - foreach ($cellIterator as $rowCell) { - if ($rowCell !== null) { - $rowCell->getStyle()->applyFromArray($pStyles); - } - } - } - - break; - case 'CELL': - $oldXfIndexes = []; - for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { - for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { - $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true; - } - } - - break; - } + $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStart, $rangeEnd, $rangeStart0, $rangeEnd0, $pStyles); // clone each of the affected styles, apply the style array, and add the new styles to the workbook $workbook = $this->getActiveSheet()->getParent(); + $newXfIndexes = []; foreach ($oldXfIndexes as $oldXfIndex => $dummy) { $style = $workbook->getCellXfByIndex($oldXfIndex); $newStyle = clone $style; @@ -472,6 +427,57 @@ class Style extends Supervisor return $this; } + private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $rangeStart0, string $rangeEnd0, array $pStyles): array + { + $oldXfIndexes = []; + switch ($selectionType) { + case 'COLUMN': + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; + } + foreach ($this->getActiveSheet()->getColumnIterator($rangeStart0, $rangeEnd0) as $columnIterator) { + $cellIterator = $columnIterator->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $columnCell) { + if ($columnCell !== null) { + $columnCell->getStyle()->applyFromArray($pStyles); + } + } + } + + break; + case 'ROW': + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + if ($this->getActiveSheet()->getRowDimension($row)->getXfIndex() === null) { + $oldXfIndexes[0] = true; // row without explicit style should be formatted based on default style + } else { + $oldXfIndexes[$this->getActiveSheet()->getRowDimension($row)->getXfIndex()] = true; + } + } + foreach ($this->getActiveSheet()->getRowIterator((int) $rangeStart[1], (int) $rangeEnd[1]) as $rowIterator) { + $cellIterator = $rowIterator->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(true); + foreach ($cellIterator as $rowCell) { + if ($rowCell !== null) { + $rowCell->getStyle()->applyFromArray($pStyles); + } + } + } + + break; + case 'CELL': + for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + $oldXfIndexes[$this->getActiveSheet()->getCellByColumnAndRow($col, $row)->getXfIndex()] = true; + } + } + + break; + } + + return $oldXfIndexes; + } + /** * Get Fill. * diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index 22fc775c..dc876ee9 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -779,6 +779,9 @@ class AutoFilter case AutoFilter\Column::AUTOFILTER_FILTERTYPE_TOPTENFILTER: $ruleValues = []; $dataRowCount = $rangeEnd[1] - $rangeStart[1]; + $toptenRuleType = null; + $ruleValue = null; + $ruleOperator = null; foreach ($rules as $rule) { // We should only ever have one Dynamic Filter Rule anyway $toptenRuleType = $rule->getGrouping(); diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index c7c2e7d6..d458fc74 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -420,6 +420,7 @@ class Xls extends BaseWriter private function processDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void { + $blipType = null; $blipData = ''; $filename = $drawing->getPath(); diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index f89957a4..98b2b5cc 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -747,7 +747,7 @@ class Parser return pack('C', 0xFF); } - private function convertDefinedName(string $name): void + private function convertDefinedName(string $name): string { if (strlen($name) > 255) { throw new WriterException('Defined Name is too long'); @@ -764,7 +764,8 @@ class Parser $ptgRef = pack('Cvxx', $this->ptg['ptgName'], $nameReference); throw new WriterException('Cannot yet write formulae with defined names to Xls'); -// return $ptgRef; + + return $ptgRef; } /** @@ -968,6 +969,7 @@ class Parser */ private function advance() { + $token = ''; $i = $this->currentCharacter; $formula_length = strlen($this->formula); // eat up white spaces diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index d2784d6d..8f6015de 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -1344,32 +1344,13 @@ class Worksheet extends BIFFwriter */ private function writeColinfo($col_array): void { - if (isset($col_array[0])) { - $colFirst = $col_array[0]; - } - if (isset($col_array[1])) { - $colLast = $col_array[1]; - } - if (isset($col_array[2])) { - $coldx = $col_array[2]; - } else { - $coldx = 8.43; - } - if (isset($col_array[3])) { - $xfIndex = $col_array[3]; - } else { - $xfIndex = 15; - } - if (isset($col_array[4])) { - $grbit = $col_array[4]; - } else { - $grbit = 0; - } - if (isset($col_array[5])) { - $level = $col_array[5]; - } else { - $level = 0; - } + $colFirst = $col_array[0] ?? null; + $colLast = $col_array[1] ?? null; + $coldx = $col_array[2] ?? 8.43; + $xfIndex = $col_array[3] ?? 15; + $grbit = $col_array[4] ?? 0; + $level = $col_array[5] ?? 0; + $record = 0x007D; // Record identifier $length = 0x000C; // Number of bytes to follow @@ -1425,13 +1406,6 @@ class Worksheet extends BIFFwriter $irefAct = 0; // Active cell ref $cref = 1; // Number of refs - if (!isset($rwLast)) { - $rwLast = $rwFirst; // Last row in reference - } - if (!isset($colLast)) { - $colLast = $colFirst; // Last col in reference - } - // Swap last row/col for first row/col as necessary if ($rwFirst > $rwLast) { [$rwFirst, $rwLast] = [$rwLast, $rwFirst]; @@ -1660,7 +1634,7 @@ class Worksheet extends BIFFwriter if (!isset($rwTop)) { $rwTop = $y; } - if (!isset($colLeft)) { + if (!$colLeft) { $colLeft = $x; } } else { @@ -1668,7 +1642,7 @@ class Worksheet extends BIFFwriter if (!isset($rwTop)) { $rwTop = 0; } - if (!isset($colLeft)) { + if (!$colLeft) { $colLeft = 0; } @@ -1684,7 +1658,7 @@ class Worksheet extends BIFFwriter // Determine which pane should be active. There is also the undocumented // option to override this should it be necessary: may be removed later. // - if (!isset($pnnAct)) { + if (!$pnnAct) { if ($x != 0 && $y != 0) { $pnnAct = 0; // Bottom right } @@ -2974,9 +2948,9 @@ class Worksheet extends BIFFwriter private function writeCFRule(Conditional $conditional): void { $record = 0x01B1; // Record identifier + $type = null; // Type of the CF + $operatorType = null; // Comparison operator - // $type : Type of the CF - // $operatorType : Comparison operator if ($conditional->getConditionType() == Conditional::CONDITION_EXPRESSION) { $type = 0x02; $operatorType = 0x00; @@ -3141,6 +3115,11 @@ class Worksheet extends BIFFwriter // Text direction $flags |= (1 == 0 ? 0x80000000 : 0); + $dataBlockFont = null; + $dataBlockAlign = null; + $dataBlockBorder = null; + $dataBlockFill = null; + // Data Blocks if ($bFormatFont == 1) { // Font Name @@ -4398,15 +4377,6 @@ class Worksheet extends BIFFwriter $dataBlockFill = pack('v', $blockFillPatternStyle); $dataBlockFill .= pack('v', $colorIdxFg | ($colorIdxBg << 7)); } - if ($bFormatProt == 1) { - $dataBlockProtection = 0; - if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) { - $dataBlockProtection = 1; - } - if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) { - $dataBlockProtection = 1 << 1; - } - } $data = pack('CCvvVv', $type, $operatorType, $szValue1, $szValue2, $flags, 0x0000); if ($bFormatFont == 1) { // Block Formatting : OK @@ -4422,7 +4392,7 @@ class Worksheet extends BIFFwriter $data .= $dataBlockFill; } if ($bFormatProt == 1) { - $data .= $dataBlockProtection; + $data .= $this->getDataBlockProtection($conditional); } if ($operand1 !== null) { $data .= $operand1; @@ -4486,4 +4456,17 @@ class Worksheet extends BIFFwriter $data .= $cellRange; $this->append($header . $data); } + + private function getDataBlockProtection(Conditional $conditional): int + { + $dataBlockProtection = 0; + if ($conditional->getStyle()->getProtection()->getLocked() == Protection::PROTECTION_PROTECTED) { + $dataBlockProtection = 1; + } + if ($conditional->getStyle()->getProtection()->getHidden() == Protection::PROTECTION_PROTECTED) { + $dataBlockProtection = 1 << 1; + } + + return $dataBlockProtection; + } } diff --git a/src/PhpSpreadsheet/Writer/Xls/Xf.php b/src/PhpSpreadsheet/Writer/Xls/Xf.php index 90d21bcf..eca3d8e0 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Xf.php +++ b/src/PhpSpreadsheet/Writer/Xls/Xf.php @@ -115,6 +115,21 @@ class Xf */ private $rightBorderColor; + /** + * @var int + */ + private $diag; + + /** + * @var int + */ + private $diagColor; + + /** + * @var Style + */ + private $style; + /** * Constructor. * @@ -132,14 +147,14 @@ class Xf $this->foregroundColor = 0x40; $this->backgroundColor = 0x41; - $this->_diag = 0; + $this->diag = 0; $this->bottomBorderColor = 0x40; $this->topBorderColor = 0x40; $this->leftBorderColor = 0x40; $this->rightBorderColor = 0x40; - $this->_diag_color = 0x40; - $this->_style = $style; + $this->diagColor = 0x40; + $this->style = $style; } /** @@ -153,39 +168,39 @@ class Xf if ($this->isStyleXf) { $style = 0xFFF5; } else { - $style = self::mapLocked($this->_style->getProtection()->getLocked()); - $style |= self::mapHidden($this->_style->getProtection()->getHidden()) << 1; + $style = self::mapLocked($this->style->getProtection()->getLocked()); + $style |= self::mapHidden($this->style->getProtection()->getHidden()) << 1; } // Flags to indicate if attributes have been set. $atr_num = ($this->numberFormatIndex != 0) ? 1 : 0; $atr_fnt = ($this->fontIndex != 0) ? 1 : 0; - $atr_alc = ((int) $this->_style->getAlignment()->getWrapText()) ? 1 : 0; - $atr_bdr = (self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) || - self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) || - self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()) || - self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle())) ? 1 : 0; + $atr_alc = ((int) $this->style->getAlignment()->getWrapText()) ? 1 : 0; + $atr_bdr = (self::mapBorderStyle($this->style->getBorders()->getBottom()->getBorderStyle()) || + self::mapBorderStyle($this->style->getBorders()->getTop()->getBorderStyle()) || + self::mapBorderStyle($this->style->getBorders()->getLeft()->getBorderStyle()) || + self::mapBorderStyle($this->style->getBorders()->getRight()->getBorderStyle())) ? 1 : 0; $atr_pat = ($this->foregroundColor != 0x40) ? 1 : 0; $atr_pat = ($this->backgroundColor != 0x41) ? 1 : $atr_pat; - $atr_pat = self::mapFillType($this->_style->getFill()->getFillType()) ? 1 : $atr_pat; - $atr_prot = self::mapLocked($this->_style->getProtection()->getLocked()) - | self::mapHidden($this->_style->getProtection()->getHidden()); + $atr_pat = self::mapFillType($this->style->getFill()->getFillType()) ? 1 : $atr_pat; + $atr_prot = self::mapLocked($this->style->getProtection()->getLocked()) + | self::mapHidden($this->style->getProtection()->getHidden()); // Zero the default border colour if the border has not been set. - if (self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) == 0) { + if (self::mapBorderStyle($this->style->getBorders()->getBottom()->getBorderStyle()) == 0) { $this->bottomBorderColor = 0; } - if (self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) == 0) { + if (self::mapBorderStyle($this->style->getBorders()->getTop()->getBorderStyle()) == 0) { $this->topBorderColor = 0; } - if (self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle()) == 0) { + if (self::mapBorderStyle($this->style->getBorders()->getRight()->getBorderStyle()) == 0) { $this->rightBorderColor = 0; } - if (self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()) == 0) { + if (self::mapBorderStyle($this->style->getBorders()->getLeft()->getBorderStyle()) == 0) { $this->leftBorderColor = 0; } - if (self::mapBorderStyle($this->_style->getBorders()->getDiagonal()->getBorderStyle()) == 0) { - $this->_diag_color = 0; + if (self::mapBorderStyle($this->style->getBorders()->getDiagonal()->getBorderStyle()) == 0) { + $this->diagColor = 0; } $record = 0x00E0; // Record identifier @@ -194,9 +209,9 @@ class Xf $ifnt = $this->fontIndex; // Index to FONT record $ifmt = $this->numberFormatIndex; // Index to FORMAT record - $align = $this->mapHAlign($this->_style->getAlignment()->getHorizontal()); // Alignment - $align |= (int) $this->_style->getAlignment()->getWrapText() << 3; - $align |= self::mapVAlign($this->_style->getAlignment()->getVertical()) << 4; + $align = $this->mapHAlign($this->style->getAlignment()->getHorizontal()); // Alignment + $align |= (int) $this->style->getAlignment()->getWrapText() << 3; + $align |= self::mapVAlign($this->style->getAlignment()->getVertical()) << 4; $align |= $this->textJustLast << 7; $used_attrib = $atr_num << 2; @@ -209,35 +224,35 @@ class Xf $icv = $this->foregroundColor; // fg and bg pattern colors $icv |= $this->backgroundColor << 7; - $border1 = self::mapBorderStyle($this->_style->getBorders()->getLeft()->getBorderStyle()); // Border line style and color - $border1 |= self::mapBorderStyle($this->_style->getBorders()->getRight()->getBorderStyle()) << 4; - $border1 |= self::mapBorderStyle($this->_style->getBorders()->getTop()->getBorderStyle()) << 8; - $border1 |= self::mapBorderStyle($this->_style->getBorders()->getBottom()->getBorderStyle()) << 12; + $border1 = self::mapBorderStyle($this->style->getBorders()->getLeft()->getBorderStyle()); // Border line style and color + $border1 |= self::mapBorderStyle($this->style->getBorders()->getRight()->getBorderStyle()) << 4; + $border1 |= self::mapBorderStyle($this->style->getBorders()->getTop()->getBorderStyle()) << 8; + $border1 |= self::mapBorderStyle($this->style->getBorders()->getBottom()->getBorderStyle()) << 12; $border1 |= $this->leftBorderColor << 16; $border1 |= $this->rightBorderColor << 23; - $diagonalDirection = $this->_style->getBorders()->getDiagonalDirection(); + $diagonalDirection = $this->style->getBorders()->getDiagonalDirection(); $diag_tl_to_rb = $diagonalDirection == Borders::DIAGONAL_BOTH - || $diagonalDirection == Borders::DIAGONAL_DOWN; + || $diagonalDirection == Borders::DIAGONAL_DOWN; $diag_tr_to_lb = $diagonalDirection == Borders::DIAGONAL_BOTH - || $diagonalDirection == Borders::DIAGONAL_UP; + || $diagonalDirection == Borders::DIAGONAL_UP; $border1 |= $diag_tl_to_rb << 30; $border1 |= $diag_tr_to_lb << 31; $border2 = $this->topBorderColor; // Border color $border2 |= $this->bottomBorderColor << 7; - $border2 |= $this->_diag_color << 14; - $border2 |= self::mapBorderStyle($this->_style->getBorders()->getDiagonal()->getBorderStyle()) << 21; - $border2 |= self::mapFillType($this->_style->getFill()->getFillType()) << 26; + $border2 |= $this->diagColor << 14; + $border2 |= self::mapBorderStyle($this->style->getBorders()->getDiagonal()->getBorderStyle()) << 21; + $border2 |= self::mapFillType($this->style->getFill()->getFillType()) << 26; $header = pack('vv', $record, $length); //BIFF8 options: identation, shrinkToFit and text direction - $biff8_options = $this->_style->getAlignment()->getIndent(); - $biff8_options |= (int) $this->_style->getAlignment()->getShrinkToFit() << 4; + $biff8_options = $this->style->getAlignment()->getIndent(); + $biff8_options |= (int) $this->style->getAlignment()->getShrinkToFit() << 4; $data = pack('vvvC', $ifnt, $ifmt, $style, $align); - $data .= pack('CCC', self::mapTextRotation($this->_style->getAlignment()->getTextRotation()), $biff8_options, $used_attrib); + $data .= pack('CCC', self::mapTextRotation($this->style->getAlignment()->getTextRotation()), $biff8_options, $used_attrib); $data .= pack('VVv', $border1, $border2, $icv); return $header . $data; @@ -300,7 +315,7 @@ class Xf */ public function setDiagColor($colorIndex): void { - $this->_diag_color = $colorIndex; + $this->diagColor = $colorIndex; } /** diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 583b262c..19da32c4 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -219,10 +219,12 @@ class Chart extends WriterPart $chartTypes = self::getChartType($plotArea); $catIsMultiLevelSeries = $valIsMultiLevelSeries = false; $plotGroupingType = ''; + $chartType = null; foreach ($chartTypes as $chartType) { $objWriter->startElement('c:' . $chartType); $groupCount = $plotArea->getPlotGroupCount(); + $plotGroup = null; for ($i = 0; $i < $groupCount; ++$i) { $plotGroup = $plotArea->getPlotGroupByIndex($i); $groupType = $plotGroup->getPlotType(); @@ -244,7 +246,7 @@ class Chart extends WriterPart $this->writeDataLabels($objWriter, $layout); - if ($chartType === DataSeries::TYPE_LINECHART) { + if ($chartType === DataSeries::TYPE_LINECHART && $plotGroup) { // Line only, Line3D can't be smoothed $objWriter->startElement('c:smooth'); $objWriter->writeAttribute('val', (int) $plotGroup->getSmoothLine()); @@ -1079,6 +1081,7 @@ class Chart extends WriterPart } } + $plotSeriesIdx = 0; foreach ($plotSeriesOrder as $plotSeriesIdx => $plotSeriesRef) { $objWriter->startElement('c:ser'); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php index f1bc51f3..2e52a5d7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/WeekDayTest.php @@ -2,7 +2,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\DateTime; -use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Weekday; +use PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\WeekDay; class WeekDayTest extends AllSetupTeardown { @@ -28,8 +28,8 @@ class WeekDayTest extends AllSetupTeardown public function testWEEKDAYwith1904Calendar(): void { self::setMac1904(); - self::assertEquals(7, Weekday::funcWeekDay('1904-01-02')); - self::assertEquals(6, Weekday::funcWeekDay('1904-01-01')); - self::assertEquals(6, Weekday::funcWeekDay(null)); + self::assertEquals(7, WeekDay::funcWeekDay('1904-01-02')); + self::assertEquals(6, WeekDay::funcWeekDay('1904-01-01')); + self::assertEquals(6, WeekDay::funcWeekDay(null)); } } diff --git a/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php b/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php index f98ff7e1..c32c5743 100644 --- a/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php @@ -37,7 +37,7 @@ class XmlScannerTest extends TestCase self::assertEquals($expectedResult, $result); // php 8.+ deprecated libxml_disable_entity_loader() - It's on by default - if (\PHP_VERSION_ID < 80000) { + if (isset($oldDisableEntityLoaderState)) { libxml_disable_entity_loader($oldDisableEntityLoaderState); } } From dd74dd7fcfc21c81a0a25796d2d9e6c491eda63d Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 3 Apr 2021 17:10:40 +0200 Subject: [PATCH 81/89] Let's start with some appeasements to phpstan, just to reduce the baseline (#1983) * Let's start with some appeasements to phpstan, just to reduce the baseline * Appeasements to phpstan, taking the number of reported errors down to just 61 --- .../Calculation/Database/DMax.php | 2 +- .../Calculation/Database/DMin.php | 2 +- .../Calculation/Database/DProduct.php | 2 +- .../Calculation/Database/DStDev.php | 2 +- .../Calculation/Database/DStDevP.php | 2 +- .../Calculation/Database/DSum.php | 2 +- .../Calculation/Database/DVar.php | 2 +- .../Calculation/Database/DVarP.php | 2 +- .../Calculation/DateTimeExcel/Days360.php | 26 +-- .../Calculation/Engineering.php | 42 ++--- .../Calculation/Engineering/BesselI.php | 12 +- .../Calculation/Engineering/BesselJ.php | 11 +- .../Calculation/Engineering/BesselK.php | 11 +- .../Calculation/Engineering/BesselY.php | 11 +- .../Calculation/Engineering/Complex.php | 8 +- .../Calculation/Engineering/Erf.php | 8 +- .../Calculation/Engineering/ErfC.php | 2 +- src/PhpSpreadsheet/Calculation/Financial.php | 48 ++--- .../Calculation/Financial/Amortization.php | 36 ++-- .../Calculation/Financial/Coupons.php | 174 +++++++++--------- .../Calculation/Financial/Depreciation.php | 42 ++--- .../Calculation/Financial/Dollar.php | 8 +- .../Calculation/Financial/InterestRate.php | 8 +- .../Financial/Securities/AccruedInterest.php | 48 +++-- .../Financial/Securities/Price.php | 52 +++--- .../Calculation/Financial/TreasuryBill.php | 6 +- src/PhpSpreadsheet/Calculation/LookupRef.php | 4 +- .../Calculation/LookupRef/Address.php | 22 +-- .../Calculation/Statistical.php | 62 +++---- .../Calculation/Statistical/Confidence.php | 6 +- .../Statistical/Distributions/Beta.php | 34 ++-- .../Statistical/Distributions/Binomial.php | 31 ++-- .../Statistical/Distributions/ChiSquared.php | 18 +- .../Statistical/Distributions/Exponential.php | 6 +- .../Statistical/Distributions/F.php | 9 +- .../Statistical/Distributions/Fisher.php | 12 +- .../Statistical/Distributions/Gamma.php | 18 +- .../Distributions/HyperGeometric.php | 8 +- .../Statistical/Distributions/LogNormal.php | 22 +-- .../Statistical/Distributions/Normal.php | 14 +- .../Statistical/Distributions/Poisson.php | 6 +- .../Distributions/StandardNormal.php | 15 +- .../Statistical/Distributions/StudentT.php | 12 +- .../Statistical/Distributions/Weibull.php | 8 +- .../Calculation/Statistical/Percentiles.php | 14 +- .../Calculation/Statistical/Permutations.php | 12 +- .../Calculation/Statistical/Trends.php | 14 +- src/PhpSpreadsheet/Calculation/TextData.php | 4 +- .../Calculation/TextData/CaseConvert.php | 8 +- .../Calculation/TextData/CharacterConvert.php | 4 +- .../Calculation/TextData/Extract.php | 14 +- .../Calculation/TextData/Format.php | 24 +-- .../Calculation/TextData/Replace.php | 16 +- .../Calculation/TextData/Search.php | 12 +- .../Calculation/TextData/Text.php | 6 +- .../Calculation/TextData/Trim.php | 4 +- .../Calculation/CalculationTest.php | 4 +- .../Functions/DateTime/TimeValueTest.php | 2 +- .../Functions/Logical/IfErrorTest.php | 4 +- .../Functions/Logical/IfNaTest.php | 4 +- .../Functions/MathTrig/EvenTest.php | 2 +- .../Functions/MathTrig/FactDoubleTest.php | 2 +- .../Functions/MathTrig/OddTest.php | 2 +- .../Functions/MathTrig/SignTest.php | 2 +- .../Functions/MathTrig/SqrtPiTest.php | 2 +- .../Functions/Statistical/FisherInvTest.php | 2 +- .../Functions/Statistical/FisherTest.php | 2 +- .../Functions/Statistical/GammaLnTest.php | 2 +- .../Functions/TextData/CharTest.php | 2 +- .../Functions/TextData/CleanTest.php | 2 +- .../Functions/TextData/CodeTest.php | 2 +- .../Functions/TextData/LeftTest.php | 2 +- .../Functions/TextData/LenTest.php | 2 +- .../Functions/TextData/LowerTest.php | 4 +- .../Functions/TextData/MidTest.php | 2 +- .../Functions/TextData/ProperTest.php | 4 +- .../Functions/TextData/RightTest.php | 2 +- .../Calculation/Functions/TextData/TTest.php | 2 +- .../Functions/TextData/TrimTest.php | 2 +- .../Functions/TextData/UpperTest.php | 4 +- .../Functions/TextData/ValueTest.php | 2 +- 81 files changed, 534 insertions(+), 536 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Database/DMax.php b/src/PhpSpreadsheet/Calculation/Database/DMax.php index 6cf2f20d..e84a0bfc 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DMax.php +++ b/src/PhpSpreadsheet/Calculation/Database/DMax.php @@ -30,7 +30,7 @@ class DMax extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float + * @return null|float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DMin.php b/src/PhpSpreadsheet/Calculation/Database/DMin.php index 5668bcf6..4398a7c3 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DMin.php +++ b/src/PhpSpreadsheet/Calculation/Database/DMin.php @@ -30,7 +30,7 @@ class DMin extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float + * @return null|float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DProduct.php b/src/PhpSpreadsheet/Calculation/Database/DProduct.php index f02eb196..4515da24 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DProduct.php +++ b/src/PhpSpreadsheet/Calculation/Database/DProduct.php @@ -29,7 +29,7 @@ class DProduct extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float|string + * @return null|float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DStDev.php b/src/PhpSpreadsheet/Calculation/Database/DStDev.php index cfc7e952..7ec42bc4 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DStDev.php +++ b/src/PhpSpreadsheet/Calculation/Database/DStDev.php @@ -30,7 +30,7 @@ class DStDev extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float|string + * @return null|float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DStDevP.php b/src/PhpSpreadsheet/Calculation/Database/DStDevP.php index 2a04c5d9..cbe241f0 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DStDevP.php +++ b/src/PhpSpreadsheet/Calculation/Database/DStDevP.php @@ -30,7 +30,7 @@ class DStDevP extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float|string + * @return null|float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DSum.php b/src/PhpSpreadsheet/Calculation/Database/DSum.php index 4f784e19..e7e28a4a 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DSum.php +++ b/src/PhpSpreadsheet/Calculation/Database/DSum.php @@ -29,7 +29,7 @@ class DSum extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float|string + * @return null|float|string */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DVar.php b/src/PhpSpreadsheet/Calculation/Database/DVar.php index c70da073..0a998c03 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DVar.php +++ b/src/PhpSpreadsheet/Calculation/Database/DVar.php @@ -30,7 +30,7 @@ class DVar extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float|string (string if result is an error) + * @return null|float|string (string if result is an error) */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/Database/DVarP.php b/src/PhpSpreadsheet/Calculation/Database/DVarP.php index f22f2cca..77acbdfc 100644 --- a/src/PhpSpreadsheet/Calculation/Database/DVarP.php +++ b/src/PhpSpreadsheet/Calculation/Database/DVarP.php @@ -30,7 +30,7 @@ class DVarP extends DatabaseAbstract * the column label in which you specify a condition for the * column. * - * @return float|string (string if result is an error) + * @return null|float|string (string if result is an error) */ public static function evaluate($database, $field, $criteria) { diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php index b90bc367..18a1abc8 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php @@ -19,20 +19,20 @@ class Days360 * DAYS360(startDate,endDate[,method]) * * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), - * PHP DateTime object, or a standard date string + * 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 mixed (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 - * the starting date is earlier than the 30th of a month, the - * ending date becomes equal to the 1st of the next month; - * otherwise the ending date becomes equal to the 30th of the - * same month. - * 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. + * PHP DateTime object, or a standard date string + * @param 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 + * the starting date is earlier than the 30th of a month, the + * ending date becomes equal to the 1st of the next month; + * otherwise the ending date becomes equal to the 30th of the + * same month. + * 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. * * @return int|string Number of days between start date and end date */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering.php b/src/PhpSpreadsheet/Calculation/Engineering.php index 229607e2..7584556a 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering.php +++ b/src/PhpSpreadsheet/Calculation/Engineering.php @@ -156,7 +156,7 @@ class Engineering * * @see Use the toDecimal() method in the Engineering\ConvertBinary class instead * - * @param string $x The binary number (as a string) that you want to convert. The number + * @param mixed $x 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. @@ -182,13 +182,13 @@ class Engineering * * @see Use the toHex() method in the Engineering\ConvertBinary class instead * - * @param string $x The binary number (as a string) that you want to convert. The number + * @param mixed $x 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 + * @param mixed $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. @@ -214,13 +214,13 @@ class Engineering * * @see Use the toOctal() method in the Engineering\ConvertBinary class instead * - * @param string $x The binary number (as a string) that you want to convert. The number + * @param mixed $x 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 + * @param mixed $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. @@ -246,7 +246,7 @@ class Engineering * * @see Use the toBinary() method in the Engineering\ConvertDecimal class instead * - * @param string $x The decimal integer you want to convert. If number is negative, + * @param mixed $x 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 @@ -256,7 +256,7 @@ class Engineering * 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 + * @param mixed $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. @@ -282,7 +282,7 @@ class Engineering * * @see Use the toHex() method in the Engineering\ConvertDecimal class instead * - * @param string $x The decimal integer you want to convert. If number is negative, + * @param mixed $x 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 @@ -292,7 +292,7 @@ class Engineering * 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 + * @param mixed $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. @@ -318,7 +318,7 @@ class Engineering * * @see Use the toOctal() method in the Engineering\ConvertDecimal class instead * - * @param string $x The decimal integer you want to convert. If number is negative, + * @param mixed $x 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 @@ -328,7 +328,7 @@ class Engineering * 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 + * @param mixed $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. @@ -354,7 +354,7 @@ class Engineering * * @see Use the toBinary() method in the Engineering\ConvertHex class instead * - * @param string $x the hexadecimal number you want to convert. + * @param mixed $x the hexadecimal number (as a string) that 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. @@ -364,7 +364,7 @@ class Engineering * 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, + * @param mixed $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. @@ -390,7 +390,7 @@ class Engineering * * @see Use the toDecimal() method in the Engineering\ConvertHex class instead * - * @param string $x The hexadecimal number you want to convert. This number cannot + * @param mixed $x The hexadecimal number (as a string) that 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 @@ -417,7 +417,7 @@ class Engineering * * @see Use the toOctal() method in the Engineering\ConvertHex class instead * - * @param string $x The hexadecimal number you want to convert. Number cannot + * @param mixed $x The hexadecimal number (as a string) that 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 @@ -430,7 +430,7 @@ class Engineering * 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 + * @param mixed $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. @@ -457,7 +457,7 @@ class Engineering * * @see Use the toBinary() method in the Engineering\ConvertOctal class instead * - * @param string $x The octal number you want to convert. Number may not + * @param mixed $x 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 @@ -470,7 +470,7 @@ class Engineering * 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, + * @param mixed $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). @@ -499,7 +499,7 @@ class Engineering * * @see Use the toDecimal() method in the Engineering\ConvertOctal class instead * - * @param string $x The octal number you want to convert. Number may not contain + * @param mixed $x 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 @@ -526,7 +526,7 @@ class Engineering * * @see Use the toHex() method in the Engineering\ConvertOctal class instead * - * @param string $x The octal number you want to convert. Number may not contain + * @param mixed $x 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 @@ -537,7 +537,7 @@ class Engineering * #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 + * @param mixed $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. diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php index 23183612..89977101 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php @@ -21,12 +21,12 @@ class BesselI * NOTE: The MS Excel implementation of the BESSELI function is still not accurate. * This code provides a more accurate calculation * - * @param mixed (float) $x The value at which to evaluate the function. - * If x is nonnumeric, BESSELI returns the #VALUE! error value. - * @param mixed (int) $ord The 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. + * @param mixed $x A float value at which to evaluate the function. + * If x is nonnumeric, BESSELI returns the #VALUE! error value. + * @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. * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php index ca9ff4f7..e16c1519 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php @@ -20,11 +20,12 @@ class BesselJ * NOTE: The MS Excel implementation of the BESSELJ function is still not accurate, particularly for higher order * values with x < -8 and x > 8. This code provides a more accurate calculation * - * @param mixed (float) $x The value at which to evaluate the function. - * If x is nonnumeric, BESSELJ returns the #VALUE! error value. - * @param mixed (int) $ord The order of the Bessel function. If n 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. + * @param mixed $x A float value at which to evaluate the function. + * If x is nonnumeric, BESSELJ returns the #VALUE! error value. + * @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. * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php index faba191f..57794e03 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php @@ -18,11 +18,12 @@ class BesselK * Excel Function: * BESSELK(x,ord) * - * @param mixed (float) $x The value at which to evaluate the function. - * If x is nonnumeric, BESSELK returns the #VALUE! error value. - * @param mixed (int) $ord The order of the Bessel function. If n is not an integer, it is truncated. - * If $ord is nonnumeric, BESSELK returns the #VALUE! error value. - * If $ord < 0, BESSELK returns the #NUM! error value. + * @param mixed $x A float value at which to evaluate the function. + * If x is nonnumeric, BESSELK returns the #VALUE! error value. + * @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. * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php index 1eed5a54..19932c64 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselY.php @@ -17,11 +17,12 @@ class BesselY * Excel Function: * BESSELY(x,ord) * - * @param mixed (float) $x The value at which to evaluate the function. - * If x is nonnumeric, BESSELY returns the #VALUE! error value. - * @param mixed (int) $ord The order of the Bessel function. If n 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. + * @param mixed $x A float value at which to evaluate the function. + * If x is nonnumeric, BESSELY returns the #VALUE! error value. + * @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. * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Complex.php b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php index a1a64768..f2718e4a 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Complex.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Complex.php @@ -19,10 +19,10 @@ class Complex * Excel Function: * COMPLEX(realNumber,imaginary[,suffix]) * - * @param mixed (float) $realNumber the real coefficient of the complex number - * @param mixed (float) $imaginary the imaginary coefficient of the complex number - * @param mixed (string) $suffix The suffix for the imaginary component of the complex number. - * If omitted, the suffix is assumed to be "i". + * @param mixed $realNumber the real float coefficient of the complex number + * @param mixed $imaginary the imaginary float coefficient of the complex number + * @param mixed $suffix The character suffix for the imaginary component of the complex number. + * If omitted, the suffix is assumed to be "i". * * @return string */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/Erf.php b/src/PhpSpreadsheet/Calculation/Engineering/Erf.php index a5df425e..db87ec0d 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/Erf.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/Erf.php @@ -21,9 +21,9 @@ class Erf * Excel Function: * ERF(lower[,upper]) * - * @param mixed (float) $lower lower bound for integrating ERF - * @param mixed (float) $upper upper bound for integrating ERF. - * If omitted, ERF integrates between zero and lower_limit + * @param mixed $lower Lower bound float for integrating ERF + * @param mixed $upper Upper bound float for integrating ERF. + * If omitted, ERF integrates between zero and lower_limit * * @return float|string */ @@ -52,7 +52,7 @@ class Erf * Excel Function: * ERF.PRECISE(limit) * - * @param mixed (float) $limit bound for integrating ERF + * @param mixed $limit Float bound for integrating ERF, other bound is zero * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php b/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php index 31c3bd75..c57a28f4 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/ErfC.php @@ -19,7 +19,7 @@ class ErfC * Excel Function: * ERFC(x) * - * @param float $value The lower bound for integrating ERFC + * @param mixed $value The float lower bound for integrating ERFC * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 547ad6b3..984d31bf 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -49,21 +49,21 @@ class Financial * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue date * when the security is traded to the buyer. - * @param mixed (float) $rate the security's annual coupon rate - * @param mixed (float) $par The security's par value. + * @param mixed $rate the security's annual coupon rate + * @param mixed $par The security's par value. * If you omit par, ACCRINT uses $1,000. - * @param mixed (int) $frequency The number of coupon payments per year. + * @param mixed $frequency The number of coupon payments per year. * Valid frequency values are: * 1 Annual * 2 Semi-Annual * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. + * @param mixed $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 * 3 Actual/365 * 4 European 30/360 - * @param mixed (bool) $calcMethod + * @param mixed $calcMethod * If true, use Issue to Settlement * If false, use FirstInterest to Settlement * @@ -106,10 +106,10 @@ class Financial * * @param mixed $issue The security's issue date * @param mixed $settlement The security's settlement (or maturity) date - * @param mixed (float) $rate The security's annual coupon rate - * @param mixed (float) $par The security's par value. + * @param mixed $rate The security's annual coupon rate + * @param mixed $par The security's par value. * If you omit par, ACCRINT uses $1,000. - * @param mixed (int) $basis The type of day count to use. + * @param mixed $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 @@ -876,11 +876,11 @@ class Financial * Excel Function: * IRR(values[,guess]) * - * @param mixed (float[]) $values An array or a reference to cells that contain numbers for which you want + * @param mixed $values An array or a reference to cells that contain numbers for which you want * to calculate the internal rate of return. * Values must contain at least one positive value and one negative value to * calculate the internal rate of return. - * @param mixed (float) $guess A number that you guess is close to the result of IRR + * @param mixed $guess A number that you guess is close to the result of IRR * * @return float|string */ @@ -986,11 +986,11 @@ class Financial * Excel Function: * MIRR(values,finance_rate, reinvestment_rate) * - * @param mixed (float[]) $values An array or a reference to cells that contain a series of payments and - * income occurring at regular intervals. - * Payments are negative value, income is positive values. - * @param mixed (float) $finance_rate The interest rate you pay on the money used in the cash flows - * @param mixed (float) $reinvestment_rate The interest rate you receive on the cash flows as you reinvest them + * @param mixed $values An array or a reference to cells that contain a series of payments and + * income occurring at regular intervals. + * Payments are negative value, income is positive values. + * @param mixed $finance_rate The interest rate you pay on the money used in the cash flows + * @param mixed $reinvestment_rate The interest rate you receive on the cash flows as you reinvest them * * @return float|string Result, or a string containing an error */ @@ -1357,20 +1357,20 @@ class Financial * Excel Function: * RATE(nper,pmt,pv[,fv[,type[,guess]]]) * - * @param mixed (float) $nper The total number of payment periods in an annuity - * @param mixed (float) $pmt The payment made each period and cannot change over the life + * @param mixed $nper The total number of payment periods in an annuity + * @param mixed $pmt The payment made each period and cannot change over the life * of the annuity. * Typically, pmt includes principal and interest but no other * fees or taxes. - * @param mixed (float) $pv The present value - the total amount that a series of future + * @param mixed $pv The present value - the total amount that a series of future * payments is worth now - * @param mixed (float) $fv The future value, or a cash balance you want to attain after + * @param mixed $fv The future value, or a cash balance you want to attain after * the last payment is made. If fv is omitted, it is assumed * to be 0 (the future value of a loan, for example, is 0). - * @param mixed (int) $type A number 0 or 1 and indicates when payments are due: + * @param mixed $type A number 0 or 1 and indicates when payments are due: * 0 or omitted At the end of the period. * 1 At the beginning of the period. - * @param mixed (float) $guess Your guess for what the rate will be. + * @param mixed $guess Your guess for what the rate will be. * If you omit guess, it is assumed to be 10 percent. * * @return float|string @@ -1429,9 +1429,9 @@ class Financial * The security settlement date is the date after the issue date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. - * @param mixed (int) $investment The amount invested in the security - * @param mixed (int) $discount The security's discount rate - * @param mixed (int) $basis The type of day count to use. + * @param mixed $investment The amount invested in the security + * @param mixed $discount The security's discount rate + * @param mixed $basis The type of day count to use. * 0 or omitted US (NASD) 30/360 * 1 Actual/actual * 2 Actual/360 diff --git a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php index 9e838a26..8b901872 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Amortization.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Amortization.php @@ -25,18 +25,18 @@ class Amortization * Excel Function: * AMORDEGRC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) * - * @param mixed (float) $cost The cost of the asset + * @param mixed $cost The float cost of the asset * @param mixed $purchased Date of the purchase of the asset * @param mixed $firstPeriod Date of the end of the first period * @param mixed $salvage The salvage value at the end of the life of the asset - * @param mixed (float) $period The period - * @param mixed (float) $rate Rate of depreciation - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * @param mixed $period the period (float) + * @param mixed $rate rate of depreciation (float) + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string (string containing the error type if there is an error) */ @@ -103,18 +103,18 @@ class Amortization * Excel Function: * AMORLINC(cost,purchased,firstPeriod,salvage,period,rate[,basis]) * - * @param mixed (float) $cost The cost of the asset + * @param mixed $cost The cost of the asset as a float * @param mixed $purchased Date of the purchase of the asset * @param mixed $firstPeriod Date of the end of the first period * @param mixed $salvage The salvage value at the end of the life of the asset - * @param mixed (float) $period The period - * @param mixed (float) $rate Rate of depreciation - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * @param mixed $period The period as a float + * @param mixed $rate Rate of depreciation as float + * @param mixed $basis Integer indicating the type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string (string containing the error type if there is an error) */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php index ce83ccb4..c24b31be 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Coupons.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Coupons.php @@ -27,21 +27,21 @@ class Coupons * COUPDAYBS(settlement,maturity,frequency[,basis]) * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue - * date when the security is traded to the buyer. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. - * @param mixed (int) $frequency the number of coupon payments per year. - * Valid frequency values are: - * 1 Annual - * 2 Semi-Annual - * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year (int). + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string */ @@ -84,21 +84,21 @@ class Coupons * COUPDAYS(settlement,maturity,frequency[,basis]) * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue - * date when the security is traded to the buyer. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. - * @param mixed $frequency the number of coupon payments per year. - * Valid frequency values are: - * 1 Annual - * 2 Semi-Annual - * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string */ @@ -149,21 +149,21 @@ class Coupons * COUPDAYSNC(settlement,maturity,frequency[,basis]) * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue - * date when the security is traded to the buyer. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. - * @param mixed $frequency the number of coupon payments per year. - * Valid frequency values are: - * 1 Annual - * 2 Semi-Annual - * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int) . + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string */ @@ -207,24 +207,24 @@ class Coupons * COUPNCD(settlement,maturity,frequency[,basis]) * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue - * date when the security is traded to the buyer. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. - * @param mixed $frequency the number of coupon payments per year. - * Valid frequency values are: - * 1 Annual - * 2 Semi-Annual - * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @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 + * depending on the value of the ReturnDateType flag */ public static function COUPNCD($settlement, $maturity, $frequency, $basis = Helpers::DAYS_PER_YEAR_NASD) { @@ -256,21 +256,21 @@ class Coupons * COUPNUM(settlement,maturity,frequency[,basis]) * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue - * date when the security is traded to the buyer. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. - * @param mixed $frequency the number of coupon payments per year. - * Valid frequency values are: - * 1 Annual - * 2 Semi-Annual - * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return int|string */ @@ -293,7 +293,7 @@ class Coupons $yearsBetweenSettlementAndMaturity = DateTimeExcel\YearFrac::funcYearFrac($settlement, $maturity, 0); - return ceil($yearsBetweenSettlementAndMaturity * $frequency); + return (int) ceil($yearsBetweenSettlementAndMaturity * $frequency); } /** @@ -305,24 +305,24 @@ class Coupons * COUPPCD(settlement,maturity,frequency[,basis]) * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue - * date when the security is traded to the buyer. + * The security settlement date is the date after the issue + * date when the security is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. - * @param mixed $frequency the number of coupon payments per year. - * Valid frequency values are: - * 1 Annual - * 2 Semi-Annual - * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * The maturity date is the date when the security expires. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use (int). + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @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 + * depending on the value of the ReturnDateType flag */ public static function COUPPCD($settlement, $maturity, $frequency, $basis = Helpers::DAYS_PER_YEAR_NASD) { diff --git a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php index 89dc226c..a918bf7d 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Depreciation.php @@ -22,15 +22,15 @@ class Depreciation * Excel Function: * DB(cost,salvage,life,period[,month]) * - * @param mixed (float) $cost Initial cost of the asset - * @param mixed (float) $salvage Value at the end of the depreciation. - * (Sometimes called the salvage value of the asset) - * @param mixed (int) $life Number of periods over which the asset is depreciated. - * (Sometimes called the useful life of the asset) - * @param mixed (int) $period The period for which you want to calculate the - * depreciation. Period must use the same units as life. - * @param mixed (int) $month Number of months in the first year. If month is omitted, - * it defaults to 12. + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation. + * (Sometimes called the salvage value of the asset) + * @param mixed $life Number of periods over which the asset is depreciated. + * (Sometimes called the useful life of the asset) + * @param mixed $period The period for which you want to calculate the + * depreciation. Period must use the same units as life. + * @param mixed $month Number of months in the first year. If month is omitted, + * it defaults to 12. * * @return float|string */ @@ -87,14 +87,14 @@ class Depreciation * Excel Function: * DDB(cost,salvage,life,period[,factor]) * - * @param mixed (float) $cost Initial cost of the asset - * @param mixed (float) $salvage Value at the end of the depreciation. + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation. * (Sometimes called the salvage value of the asset) - * @param mixed (int) $life Number of periods over which the asset is depreciated. + * @param mixed $life Number of periods over which the asset is depreciated. * (Sometimes called the useful life of the asset) - * @param mixed (int) $period The period for which you want to calculate the + * @param mixed $period The period for which you want to calculate the * depreciation. Period must use the same units as life. - * @param mixed (float) $factor The rate at which the balance declines. + * @param mixed $factor The rate at which the balance declines. * If factor is omitted, it is assumed to be 2 (the * double-declining balance method). * @@ -139,9 +139,9 @@ class Depreciation * * Returns the straight-line depreciation of an asset for one period * - * @param mixed (float) $cost Initial cost of the asset - * @param mixed (float) $salvage Value at the end of the depreciation - * @param mixed (float) $life Number of periods over which the asset is depreciated + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation + * @param mixed $life Number of periods over which the asset is depreciated * * @return float|string Result, or a string containing an error */ @@ -171,10 +171,10 @@ class Depreciation * * Returns the sum-of-years' digits depreciation of an asset for a specified period. * - * @param mixed (float) $cost Initial cost of the asset - * @param mixed (float) $salvage Value at the end of the depreciation - * @param mixed (float) $life Number of periods over which the asset is depreciated - * @param mixed (float) $period Period + * @param mixed $cost Initial cost of the asset + * @param mixed $salvage Value at the end of the depreciation + * @param mixed $life Number of periods over which the asset is depreciated + * @param mixed $period Period * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/Dollar.php b/src/PhpSpreadsheet/Calculation/Financial/Dollar.php index 36326a60..b25b0c2e 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Dollar.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Dollar.php @@ -16,8 +16,8 @@ class Dollar * Excel Function: * DOLLARDE(fractional_dollar,fraction) * - * @param mixed (float) $fractionalDollar Fractional Dollar - * @param mixed (int) $fraction Fraction + * @param mixed $fractionalDollar Fractional Dollar + * @param mixed $fraction Fraction * * @return float|string */ @@ -52,8 +52,8 @@ class Dollar * Excel Function: * DOLLARFR(decimal_dollar,fraction) * - * @param mixed (float) $decimalDollar Decimal Dollar - * @param mixed (int) $fraction Fraction + * @param mixed $decimalDollar Decimal Dollar + * @param mixed $fraction Fraction * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php b/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php index ed0fec75..7d66c891 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php +++ b/src/PhpSpreadsheet/Calculation/Financial/InterestRate.php @@ -18,8 +18,8 @@ class InterestRate * Excel Function: * EFFECT(nominal_rate,npery) * - * @param mixed (float) $nominalRate Nominal interest rate - * @param mixed (int) $periodsPerYear Number of compounding payments per year + * @param mixed $nominalRate Nominal interest rate as a float + * @param mixed $periodsPerYear Integer number of compounding payments per year * * @return float|string */ @@ -47,8 +47,8 @@ class InterestRate * * Returns the nominal interest rate given the effective rate and the number of compounding payments per year. * - * @param mixed (float) $effectiveRate Effective interest rate - * @param mixed (int) $periodsPerYear Number of compounding payments per year + * @param mixed $effectiveRate Effective interest rate as a float + * @param mixed $periodsPerYear Integer number of compounding payments per year * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php index 726b125a..1875b8b7 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/AccruedInterest.php @@ -27,21 +27,20 @@ class AccruedInterest * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue date * when the security is traded to the buyer. - * @param mixed (float) $rate The security's annual coupon rate - * @param mixed (float) $par The security's par value. - * If you omit par, ACCRINT uses $1,000. - * @param mixed (int) $frequency The number of coupon payments per year. - * Valid frequency values are: - * 1 Annual - * 2 Semi-Annual - * 4 Quarterly - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 - * @param mixed $parValue + * @param mixed $rate The security's annual coupon rate + * @param mixed $parValue The security's par value. + * If you omit par, ACCRINT uses $1,000. + * @param mixed $frequency The number of coupon payments per year. + * Valid frequency values are: + * 1 Annual + * 2 Semi-Annual + * 4 Quarterly + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * @param mixed $calcMethod * * @return float|string Result, or a string containing an error @@ -100,16 +99,15 @@ class AccruedInterest * * @param mixed $issue The security's issue date * @param mixed $settlement The security's settlement (or maturity) date - * @param mixed (float) $rate The security's annual coupon rate - * @param mixed (float) $par The security's par value. - * If you omit par, ACCRINT uses $1,000. - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 - * @param mixed $parValue + * @param mixed $rate The security's annual coupon rate + * @param mixed $parValue The security's par value. + * If you omit par, ACCRINT uses $1,000. + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php index 6b04d6d9..cccdb78b 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php +++ b/src/PhpSpreadsheet/Calculation/Financial/Securities/Price.php @@ -22,19 +22,19 @@ class Price * is traded to the buyer. * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. - * @param mixed (float) $rate the security's annual coupon rate - * @param mixed (float) $yield the security's annual yield - * @param mixed (float) $redemption The number of coupon payments per year. + * @param mixed $rate the security's annual coupon rate + * @param mixed $yield the security's annual yield + * @param mixed $redemption The number of coupon payments per year. * For annual payments, frequency = 1; * for semiannual, frequency = 2; * for quarterly, frequency = 4. - * @param mixed (int) $frequency - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * @param mixed $frequency + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string Result, or a string containing an error */ @@ -89,14 +89,14 @@ class Price * is traded to the buyer. * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. - * @param mixed (float) $discount The security's discount rate - * @param mixed (float) $redemption The security's redemption value per $100 face value - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * @param mixed $discount The security's discount rate + * @param mixed $redemption The security's redemption value per $100 face value + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string Result, or a string containing an error */ @@ -139,14 +139,14 @@ class Price * @param mixed $maturity The security's maturity date. * The maturity date is the date when the security expires. * @param mixed $issue The security's issue date - * @param mixed (float) $rate The security's interest rate at date of issue - * @param mixed (float) $yield The security's annual yield - * @param mixed (int) $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * @param mixed $rate The security's interest rate at date of issue + * @param mixed $yield The security's annual yield + * @param mixed $basis The type of day count to use. + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php index 8fd47ba6..8f170488 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php +++ b/src/PhpSpreadsheet/Calculation/Financial/TreasuryBill.php @@ -20,7 +20,7 @@ class TreasuryBill * when the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. - * @param mixed (int) $discount The Treasury bill's discount rate + * @param mixed $discount The Treasury bill's discount rate * * @return float|string Result, or a string containing an error */ @@ -66,7 +66,7 @@ class TreasuryBill * when the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. - * @param mixed (int) $discount The Treasury bill's discount rate + * @param mixed $discount The Treasury bill's discount rate * * @return float|string Result, or a string containing an error */ @@ -117,7 +117,7 @@ class TreasuryBill * the Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. - * @param mixed (int) $price The Treasury bill's price per $100 face value + * @param mixed $price The Treasury bill's price per $100 face value * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef.php b/src/PhpSpreadsheet/Calculation/LookupRef.php index 4a1bcb06..6a89f7da 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef.php @@ -148,8 +148,8 @@ class LookupRef * Excel Function: * =HYPERLINK(linkURL,displayName) * - * @param mixed (string) $linkURL Value to check, is also the value returned when no error - * @param mixed (string) $displayName Value to return when testValue is an error condition + * @param mixed $linkURL URL Value to check, is also the value returned when no error + * @param mixed $displayName String Value to return when testValue is an error condition * @param Cell $pCell The cell to set the hyperlink in * * @return mixed The value of $displayName (or $linkURL if $displayName was blank) diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php index daaebea2..c217a1e4 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Address.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Address.php @@ -23,17 +23,17 @@ class Address * Excel Function: * =ADDRESS(row, column, [relativity], [referenceStyle], [sheetText]) * - * @param mixed $row Row number to use in the cell reference - * @param mixed $column Column number to use in the cell reference - * @param mixed (int) $relativity Flag indicating the type of reference to return - * 1 or omitted Absolute - * 2 Absolute row; relative column - * 3 Relative row; absolute column - * 4 Relative - * @param mixed (bool) $referenceStyle A logical 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 - * @param mixed (string) $sheetName Optional Name of worksheet to use + * @param mixed $row Row number (integer) to use in the cell reference + * @param mixed $column Column number (integer) to use in the cell reference + * @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 + * @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 + * @param mixed $sheetName Optional Name of worksheet to use * * @return string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical.php b/src/PhpSpreadsheet/Calculation/Statistical.php index 3e90b21d..003f06be 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical.php +++ b/src/PhpSpreadsheet/Calculation/Statistical.php @@ -172,10 +172,10 @@ class Statistical * @see Statistical\Distributions\Binomial::distribution() * Use the distribution() method in the Statistical\Distributions\Binomial class instead * - * @param mixed (float) $value Number of successes in trials - * @param mixed (float) $trials Number of trials - * @param mixed (float) $probability Probability of success on each trial - * @param mixed (bool) $cumulative + * @param mixed $value Number of successes in trials + * @param mixed $trials Number of trials + * @param mixed $probability Probability of success on each trial + * @param mixed $cumulative * * @return float|string */ @@ -790,10 +790,10 @@ class Statistical * @see Statistical\Distributions\HyperGeometric::distribution() * Use the distribution() method in the Statistical\Distributions\HyperGeometric class instead * - * @param mixed (int) $sampleSuccesses Number of successes in the sample - * @param mixed (int) $sampleNumber Size of the sample - * @param mixed (int) $populationSuccesses Number of successes in the population - * @param mixed (int) $populationNumber Population size + * @param mixed $sampleSuccesses Number of successes in the sample + * @param mixed $sampleNumber Size of the sample + * @param mixed $populationSuccesses Number of successes in the population + * @param mixed $populationNumber Population size * * @return float|string */ @@ -1221,9 +1221,9 @@ class Statistical * @see Statistical\Distributions\Binomial::negative() * Use the negative() method in the Statistical\Distributions\Binomial class instead * - * @param mixed (float) $failures Number of Failures - * @param mixed (float) $successes Threshold number of Successes - * @param mixed (float) $probability Probability of success on each trial + * @param mixed $failures Number of Failures + * @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 */ @@ -1244,10 +1244,10 @@ class Statistical * @see Statistical\Distributions\Normal::distribution() * Use the distribution() method in the Statistical\Distributions\Normal class instead * - * @param mixed (float) $value - * @param mixed (float) $mean Mean Value - * @param mixed (float) $stdDev Standard Deviation - * @param mixed (bool) $cumulative + * @param mixed $value + * @param mixed $mean Mean Value + * @param mixed $stdDev Standard Deviation + * @param mixed $cumulative * * @return float|string The result, or a string containing an error */ @@ -1266,9 +1266,9 @@ class Statistical * @see Statistical\Distributions\Normal::inverse() * Use the inverse() method in the Statistical\Distributions\Normal class instead * - * @param mixed (float) $probability - * @param mixed (float) $mean Mean Value - * @param mixed (float) $stdDev Standard Deviation + * @param mixed $probability + * @param mixed $mean Mean Value + * @param mixed $stdDev Standard Deviation * * @return float|string The result, or a string containing an error */ @@ -1289,7 +1289,7 @@ class Statistical * @see Statistical\Distributions\StandardNormal::cumulative() * Use the cumulative() method in the Statistical\Distributions\StandardNormal class instead * - * @param mixed (float) $value + * @param mixed $value * * @return float|string The result, or a string containing an error */ @@ -1310,8 +1310,8 @@ class Statistical * @see Statistical\Distributions\StandardNormal::distribution() * Use the distribution() method in the Statistical\Distributions\StandardNormal class instead * - * @param mixed (float) $value - * @param mixed (bool) $cumulative + * @param mixed $value + * @param mixed $cumulative * * @return float|string The result, or a string containing an error */ @@ -1330,7 +1330,7 @@ class Statistical * @see Statistical\Distributions\StandardNormal::inverse() * Use the inverse() method in the Statistical\Distributions\StandardNormal class instead * - * @param mixed (float) $value + * @param mixed $value * * @return float|string The result, or a string containing an error */ @@ -1374,9 +1374,9 @@ class Statistical * @see Statistical\Percentiles::PERCENTRANK() * Use the PERCENTRANK() method in the Statistical\Percentiles class instead * - * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers - * @param mixed (int) $value the number whose rank you want to find - * @param mixed (int) $significance the number of significant digits for the returned percentage value + * @param mixed $valueSet An array of, or a reference to, a list of numbers + * @param mixed $value the number whose rank you want to find + * @param mixed $significance the number of significant digits for the returned percentage value * * @return float|string (string if result is an error) */ @@ -1421,9 +1421,9 @@ class Statistical * @see Statistical\Distributions\Poisson::distribution() * Use the distribution() method in the Statistical\Distributions\Poisson class instead * - * @param mixed (float) $value - * @param mixed (float) $mean Mean Value - * @param mixed (bool) $cumulative + * @param mixed $value + * @param mixed $mean Mean Value + * @param mixed $cumulative * * @return float|string The result, or a string containing an error */ @@ -1464,9 +1464,9 @@ class Statistical * @see Statistical\Percentiles::RANK() * Use the RANK() method in the Statistical\Percentiles class instead * - * @param mixed (float) $value the number whose rank you want to find - * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers - * @param mixed (int) $order Order to sort the values in the value set + * @param mixed $value the number whose rank you want to find + * @param mixed $valueSet An array of, or a reference to, a list of numbers + * @param mixed $order Order to sort the values in the value set * * @return float|string The result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php index 40adc9e3..7d354dfe 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Confidence.php @@ -14,9 +14,9 @@ class Confidence * * Returns the confidence interval for a population mean * - * @param mixed (float) $alpha - * @param mixed (float) $stdDev Standard Deviation - * @param mixed (float) $size + * @param mixed $alpha As a float + * @param mixed $stdDev Standard Deviation as a float + * @param mixed $size As an integer * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php index 30b8d02a..95446c45 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Beta.php @@ -20,11 +20,11 @@ class Beta * * Returns the beta distribution. * - * @param mixed (float) $value Value at which you want to evaluate the distribution - * @param mixed (float) $alpha Parameter to the distribution - * @param mixed (float) $beta Parameter to the distribution - * @param mixed (float) $rMin - * @param mixed (float) $rMax + * @param mixed $value Float value at which you want to evaluate the distribution + * @param mixed $alpha Parameter to the distribution as a float + * @param mixed $beta Parameter to the distribution as a float + * @param mixed $rMin as an float + * @param mixed $rMax as an float * * @return float|string */ @@ -66,11 +66,11 @@ class Beta * * Returns the inverse of the Beta distribution. * - * @param mixed (float) $probability Probability at which you want to evaluate the distribution - * @param mixed (float) $alpha Parameter to the distribution - * @param mixed (float) $beta Parameter to the distribution - * @param mixed (float) $rMin Minimum value - * @param mixed (float) $rMax Maximum value + * @param mixed $probability Float probability at which you want to evaluate the distribution + * @param mixed $alpha Parameter to the distribution as a float + * @param mixed $beta Parameter to the distribution as a float + * @param mixed $rMin Minimum value as a float + * @param mixed $rMax Maximum value as a float * * @return float|string */ @@ -137,9 +137,9 @@ class Beta * * The computation is based on formulas from Numerical Recipes, Chapter 6.4 (W.H. Press et al, 1992). * - * @param mixed $x require 0<=x<=1 - * @param mixed $p require p>0 - * @param mixed $q require q>0 + * @param float $x require 0<=x<=1 + * @param float $p require p>0 + * @param float $q require q>0 * * @return float 0 if x<0, p<=0, q<=0 or p+q>2.55E305 and 1 if x>1 to avoid errors and over/underflow */ @@ -171,8 +171,8 @@ class Beta /** * The natural logarithm of the beta function. * - * @param mixed $p require p>0 - * @param mixed $q require q>0 + * @param float $p require p>0 + * @param float $q require q>0 * * @return float 0 if p<=0, q<=0 or p+q>2.55E305 to avoid errors and over/underflow * @@ -198,10 +198,6 @@ class Beta * Based on an idea from Numerical Recipes (W.H. Press et al, 1992). * * @author Jaco van Kooten - * - * @param mixed $x - * @param mixed $p - * @param mixed $q */ private static function betaFraction(float $x, float $p, float $q): float { diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php index 2ab1fe67..acdda8d3 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Binomial.php @@ -19,10 +19,10 @@ class Binomial * experiment. For example, BINOMDIST can calculate the probability that two of the next three * babies born are male. * - * @param mixed (int) $value Number of successes in trials - * @param mixed (int) $trials Number of trials - * @param mixed (float) $probability Probability of success on each trial - * @param mixed (bool) $cumulative + * @param mixed $value Integer number of successes in trials + * @param mixed $trials Integer umber of trials + * @param mixed $probability Probability of success on each trial as a float + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string */ @@ -58,10 +58,11 @@ class Binomial * Returns returns the Binomial Distribution probability for the number of successes from a specified number * of trials falling into a specified range. * - * @param mixed (int) $trials Number of trials - * @param mixed (float) $probability Probability of success on each trial - * @param mixed (int) $successes The number of successes in trials - * @param mixed (int) $limit Upper limit for successes in trials + * @param mixed $trials Integer number of trials + * @param mixed $probability Probability of success on each trial as a float + * @param mixed $successes The integer number of successes in trials + * @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 * * @return float|string */ @@ -105,9 +106,9 @@ class Binomial * distribution, except that the number of successes is fixed, and the number of trials is * variable. Like the binomial, trials are assumed to be independent. * - * @param mixed (float) $failures Number of Failures - * @param mixed (float) $successes Threshold number of Successes - * @param mixed (float) $probability Probability of success on each trial + * @param mixed $failures Number of Failures as an integer + * @param mixed $successes Threshold number of Successes as an integer + * @param mixed $probability Probability of success on each trial as a float * * @return float|string The result, or a string containing an error * @@ -147,9 +148,9 @@ class Binomial * Returns the smallest value for which the cumulative binomial distribution is greater * than or equal to a criterion value * - * @param float $trials number of Bernoulli trials - * @param float $probability probability of a success on each trial - * @param float $alpha criterion value + * @param mixed $trials number of Bernoulli trials as an integer + * @param mixed $probability probability of a success on each trial as a float + * @param mixed $alpha criterion value as a float * * @return int|string */ @@ -174,7 +175,7 @@ class Binomial } $successes = 0; - while (true && $successes <= $trials) { + while ($successes <= $trials) { $result = self::calculateCumulativeBinomial($successes, $trials, $probability); if ($result >= $alpha) { break; diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php index 3ebe1dc5..efc62f83 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/ChiSquared.php @@ -18,8 +18,8 @@ class ChiSquared * * Returns the one-tailed probability of the chi-squared distribution. * - * @param mixed (float) $value Value for the function - * @param mixed (int) $degrees degrees of freedom + * @param mixed $value Float value for which we want the probability + * @param mixed $degrees Integer degrees of freedom * * @return float|string */ @@ -54,9 +54,9 @@ class ChiSquared * * Returns the one-tailed probability of the chi-squared distribution. * - * @param mixed (float) $value Value for the function - * @param mixed (int) $degrees degrees of freedom - * @param mixed $cumulative + * @param mixed $value Float value for which we want the probability + * @param mixed $degrees Integer degrees of freedom + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string */ @@ -98,8 +98,8 @@ class ChiSquared * * Returns the inverse of the right-tailed probability of the chi-squared distribution. * - * @param mixed (float) $probability Probability for the function - * @param mixed (int) $degrees degrees of freedom + * @param mixed $probability Float probability at which you want to evaluate the distribution + * @param mixed $degrees Integer degrees of freedom * * @return float|string */ @@ -134,8 +134,8 @@ class ChiSquared * * Returns the inverse of the left-tailed probability of the chi-squared distribution. * - * @param mixed (float) $probability Probability for the function - * @param mixed (int) $degrees degrees of freedom + * @param mixed $probability Float probability at which you want to evaluate the distribution + * @param mixed $degrees Integer degrees of freedom * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php index fe76816d..7cf60344 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Exponential.php @@ -16,9 +16,9 @@ class Exponential * such as how long an automated bank teller takes to deliver cash. For example, you can * use EXPONDIST to determine the probability that the process takes at most 1 minute. * - * @param mixed (float) $value Value of the function - * @param mixed (float) $lambda The parameter value - * @param mixed (bool) $cumulative + * @param mixed $value Float value for which we want the probability + * @param mixed $lambda The parameter value as a float + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php index 84456873..aaf5f0df 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/F.php @@ -17,11 +17,10 @@ class F * For example, you can examine the test scores of men and women entering high school, and determine * if the variability in the females is different from that found in the males. * - * @param mixed(float) $value Value of the function - * @param mixed(int) $u The numerator degrees of freedom - * @param mixed(int) $v The denominator degrees of freedom - * @param mixed(bool) $cumulative If cumulative is TRUE, F.DIST returns the cumulative distribution function; - * if FALSE, it returns the probability density function. + * @param mixed $value Float value for which we want the probability + * @param mixed $u The numerator degrees of freedom as an integer + * @param mixed $v The denominator degrees of freedom as an integer + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php index 1d3a7be4..fd7986b0 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Fisher.php @@ -16,7 +16,7 @@ class Fisher * is normally distributed rather than skewed. Use this function to perform hypothesis * testing on the correlation coefficient. * - * @param mixed (float) $value + * @param mixed $value Float value for which we want the probability * * @return float|string */ @@ -44,20 +44,20 @@ class Fisher * analyzing correlations between ranges or arrays of data. If y = FISHER(x), then * FISHERINV(y) = x. * - * @param mixed (float) $value + * @param mixed $probability Float probability at which you want to evaluate the distribution * * @return float|string */ - public static function inverse($value) + public static function inverse($probability) { - $value = Functions::flattenSingleValue($value); + $probability = Functions::flattenSingleValue($probability); try { - self::validateFloat($value); + self::validateFloat($probability); } catch (Exception $e) { return $e->getMessage(); } - return (exp(2 * $value) - 1) / (exp(2 * $value) + 1); + return (exp(2 * $probability) - 1) / (exp(2 * $probability) + 1); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php index 2ea28391..aed25f19 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Gamma.php @@ -14,7 +14,7 @@ class Gamma extends GammaBase * * Return the gamma function value. * - * @param mixed (float) $value + * @param mixed $value Float value for which we want the probability * * @return float|string The result, or a string containing an error */ @@ -40,10 +40,10 @@ class Gamma extends GammaBase * * Returns the gamma distribution. * - * @param mixed (float) $value Value at which you want to evaluate the distribution - * @param mixed (float) $a Parameter to the distribution - * @param mixed (float) $b Parameter to the distribution - * @param mixed (bool) $cumulative + * @param mixed $value Float Value at which you want to evaluate the distribution + * @param mixed $a Parameter to the distribution as a float + * @param mixed $b Parameter to the distribution as a float + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string */ @@ -74,9 +74,9 @@ class Gamma extends GammaBase * * Returns the inverse of the Gamma distribution. * - * @param mixed (float) $probability Probability at which you want to evaluate the distribution - * @param mixed (float) $alpha Parameter to the distribution - * @param mixed (float) $beta Parameter to the distribution + * @param mixed $probability Float probability at which you want to evaluate the distribution + * @param mixed $alpha Parameter to the distribution as a float + * @param mixed $beta Parameter to the distribution as a float * * @return float|string */ @@ -106,7 +106,7 @@ class Gamma extends GammaBase * * Returns the natural logarithm of the gamma function. * - * @param mixed (float) $value + * @param mixed $value Float Value at which you want to evaluate the distribution * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php index e9848ed4..487e0d46 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/HyperGeometric.php @@ -16,10 +16,10 @@ class HyperGeometric * Returns the hypergeometric distribution. HYPGEOMDIST returns the probability of a given number of * sample successes, given the sample size, population successes, and population size. * - * @param mixed (int) $sampleSuccesses Number of successes in the sample - * @param mixed (int) $sampleNumber Size of the sample - * @param mixed (int) $populationSuccesses Number of successes in the population - * @param mixed (int) $populationNumber Population size + * @param mixed $sampleSuccesses Integer number of successes in the sample + * @param mixed $sampleNumber Integer size of the sample + * @param mixed $populationSuccesses Integer number of successes in the population + * @param mixed $populationNumber Integer population size * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php index 87b464fa..79d19ccf 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/LogNormal.php @@ -15,9 +15,9 @@ class LogNormal * Returns the cumulative lognormal distribution of x, where ln(x) is normally distributed * with parameters mean and standard_dev. * - * @param mixed (float) $value - * @param mixed (float) $mean - * @param mixed (float) $stdDev + * @param mixed $value Float value for which we want the probability + * @param mixed $mean Mean value as a float + * @param mixed $stdDev Standard Deviation as a float * * @return float|string The result, or a string containing an error */ @@ -48,10 +48,10 @@ class LogNormal * Returns the lognormal distribution of x, where ln(x) is normally distributed * with parameters mean and standard_dev. * - * @param mixed (float) $value - * @param mixed (float) $mean - * @param mixed (float) $stdDev - * @param mixed (bool) $cumulative + * @param mixed $value Float value for which we want the probability + * @param mixed $mean Mean value as a float + * @param mixed $stdDev Standard Deviation as a float + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string The result, or a string containing an error */ @@ -86,11 +86,11 @@ class LogNormal /** * LOGINV. * - * Returns the inverse of the normal cumulative distribution + * Returns the inverse of the lognormal cumulative distribution * - * @param mixed (float) $probability - * @param mixed (float) $mean - * @param mixed (float) $stdDev + * @param mixed $probability Float probability for which we want the value + * @param mixed $mean Mean Value as a float + * @param mixed $stdDev Standard Deviation as a float * * @return float|string The result, or a string containing an error * diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php index b0c5552a..b24c0ecf 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Normal.php @@ -19,10 +19,10 @@ class Normal * function has a very wide range of applications in statistics, including hypothesis * testing. * - * @param mixed (float) $value - * @param mixed (float) $mean Mean Value - * @param mixed (float) $stdDev Standard Deviation - * @param mixed (bool) $cumulative + * @param mixed $value Float value for which we want the probability + * @param mixed $mean Mean value as a float + * @param mixed $stdDev Standard Deviation as a float + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string The result, or a string containing an error */ @@ -57,9 +57,9 @@ class Normal * * Returns the inverse of the normal cumulative distribution for the specified mean and standard deviation. * - * @param mixed (float) $probability - * @param mixed (float) $mean Mean Value - * @param mixed (float) $stdDev Standard Deviation + * @param mixed $probability Float probability for which we want the value + * @param mixed $mean Mean Value as a float + * @param mixed $stdDev Standard Deviation as a float * * @return float|string The result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php index 51d097b3..e6d758e0 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Poisson.php @@ -17,9 +17,9 @@ class Poisson * is predicting the number of events over a specific time, such as the number of * cars arriving at a toll plaza in 1 minute. * - * @param mixed (float) $value - * @param mixed (float) $mean Mean Value - * @param mixed (bool) $cumulative + * @param mixed $value Float value for which we want the probability + * @param mixed $mean Mean value as a float + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string The result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php index c3049f6d..0dde2006 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StandardNormal.php @@ -15,7 +15,7 @@ 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 (float) $value + * @param mixed $value Float value for which we want the probability * * @return float|string The result, or a string containing an error */ @@ -31,8 +31,8 @@ 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 (float) $value - * @param mixed (bool) $cumulative + * @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) * * @return float|string The result, or a string containing an error */ @@ -46,7 +46,7 @@ class StandardNormal * * Returns the inverse of the standard normal cumulative distribution * - * @param mixed (float) $value + * @param mixed $value Float probability for which we want the value * * @return float|string The result, or a string containing an error */ @@ -63,9 +63,10 @@ class StandardNormal * For a given hypothesized population mean, x, Z.TEST returns the probability that the sample mean would be * greater than the average of observations in the data set (array) — that is, the observed sample mean. * - * @param mixed (float) $dataSet - * @param mixed (float) $m0 Alpha Parameter - * @param mixed (null|float) $sigma Beta Parameter + * @param mixed $dataSet The dataset should be an array of float values for the observations + * @param mixed $m0 Alpha Parameter + * @param mixed $sigma A null or float value for the Beta (Standard Deviation) Parameter; + * if null, we use the standard deviation of the dataset * * @return float|string (string if result is an error) */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php index ed02fe4d..79113bad 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/StudentT.php @@ -16,9 +16,9 @@ class StudentT * * Returns the probability of Student's T distribution. * - * @param mixed (float) $value Value for the function - * @param mixed (float) $degrees degrees of freedom - * @param mixed (int) $tails number of tails (1 or 2) + * @param mixed $value Float value for the distribution + * @param mixed $degrees Integer value for degrees of freedom + * @param mixed $tails Integer value for the number of tails (1 or 2) * * @return float|string The result, or a string containing an error */ @@ -48,8 +48,8 @@ class StudentT * * Returns the one-tailed probability of the chi-squared distribution. * - * @param mixed (float) $probability Probability for the function - * @param mixed (float) $degrees degrees of freedom + * @param mixed $probability Float probability for the function + * @param mixed $degrees Integer value for degrees of freedom * * @return float|string The result, or a string containing an error */ @@ -79,7 +79,7 @@ class StudentT } /** - * @return float|int + * @return float */ private static function calculateDistribution(float $value, int $degrees, int $tails) { diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php index 2064c2e2..5c28e69a 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Distributions/Weibull.php @@ -15,10 +15,10 @@ class Weibull * Returns the Weibull distribution. Use this distribution in reliability * analysis, such as calculating a device's mean time to failure. * - * @param mixed (float) $value - * @param mixed (float) $alpha Alpha Parameter - * @param mixed (float) $beta Beta Parameter - * @param mixed (bool) $cumulative + * @param mixed $value Float value for the distribution + * @param mixed $alpha Float alpha Parameter + * @param mixed $beta Float beta Parameter + * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false) * * @return float|string (string if result is an error) */ diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php b/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php index 0001b7bf..1f454247 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Percentiles.php @@ -69,9 +69,9 @@ class Percentiles * rather than floored (as MS Excel), so value 3 for a value set of 1, 2, 3, 4 will return * 0.667 rather than 0.666 * - * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers - * @param mixed (int) $value the number whose rank you want to find - * @param mixed (int) $significance the number of significant digits for the returned percentage value + * @param mixed $valueSet An array of (float) values, or a reference to, a list of numbers + * @param mixed $value The number whose rank you want to find + * @param mixed $significance The (integer) number of significant digits for the returned percentage value * * @return float|string (string if result is an error) */ @@ -151,11 +151,11 @@ class Percentiles * * Returns the rank of a number in a list of numbers. * - * @param mixed (float) $value the number whose rank you want to find - * @param mixed (float[]) $valueSet An array of, or a reference to, a list of numbers - * @param mixed (int) $order Order to sort the values in the value set + * @param mixed $value The number whose rank you want to find + * @param mixed $valueSet An array of float values, or a reference to, a list of numbers + * @param mixed $order Order to sort the values in the value set * - * @return float|string The result, or a string containing an error + * @return float|string The result, or a string containing an error (0 = Descending, 1 = Ascending) */ public static function RANK($value, $valueSet, $order = self::RANK_SORT_DESCENDING) { diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php index 84cdfea1..c381d718 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Permutations.php @@ -19,8 +19,8 @@ class Permutations * combinations, for which the internal order is not significant. Use this function * for lottery-style probability calculations. * - * @param mixed (int) $numObjs Number of different objects - * @param mixed (int) $numInSet Number of objects in each permutation + * @param mixed $numObjs Integer number of different objects + * @param mixed $numInSet Integer number of objects in each permutation * * @return int|string Number of permutations, or a string containing an error */ @@ -40,7 +40,7 @@ class Permutations return Functions::NAN(); } - return round(MathTrig\Fact::funcFact($numObjs) / MathTrig\Fact::funcFact($numObjs - $numInSet)); + return (int) round(MathTrig\Fact::funcFact($numObjs) / MathTrig\Fact::funcFact($numObjs - $numInSet)); } /** @@ -49,8 +49,8 @@ class Permutations * Returns the number of permutations for a given number of objects (with repetitions) * that can be selected from the total objects. * - * @param int $numObjs Number of different objects - * @param int $numInSet Number of objects in each permutation + * @param mixed $numObjs Integer number of different objects + * @param mixed $numInSet Integer number of objects in each permutation * * @return int|string Number of permutations, or a string containing an error */ @@ -70,6 +70,6 @@ class Permutations return Functions::NAN(); } - return $numObjs ** $numInSet; + return (int) ($numObjs ** $numInSet); } } diff --git a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php index 8c88c54c..e745d1b7 100644 --- a/src/PhpSpreadsheet/Calculation/Statistical/Trends.php +++ b/src/PhpSpreadsheet/Calculation/Statistical/Trends.php @@ -109,7 +109,7 @@ class Trends * Calculates, or predicts, a future value by using existing values. * The predicted value is a y-value for a given x-value. * - * @param mixed (float) $xValue Value of X for which we want to find Y + * @param mixed $xValue Float value of X for which we want to find Y * @param mixed $yValues array of mixed Data Series Y * @param mixed $xValues of mixed Data Series X * @@ -140,7 +140,7 @@ class Trends * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @param mixed[] $newValues Values of X for which we want to find Y - * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not * * @return array of float */ @@ -196,8 +196,8 @@ class Trends * * @param mixed[] $yValues Data Series Y * @param null|mixed[] $xValues Data Series X - * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 - * @param mixed (bool) $stats a logical value specifying whether to return additional regression statistics + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not + * @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics * * @return array|int|string The result, or a string containing an error */ @@ -257,8 +257,8 @@ class Trends * * @param mixed[] $yValues Data Series Y * @param null|mixed[] $xValues Data Series X - * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 - * @param mixed (bool) $stats a logical value specifying whether to return additional regression statistics + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not + * @param mixed $stats A logical (boolean) value specifying whether to return additional regression statistics * * @return array|int|string The result, or a string containing an error */ @@ -397,7 +397,7 @@ class Trends * @param mixed[] $yValues Data Series Y * @param mixed[] $xValues Data Series X * @param mixed[] $newValues Values of X for which we want to find Y - * @param mixed (bool) $const a logical value specifying whether to force the intersect to equal 0 + * @param mixed $const A logical (boolean) value specifying whether to force the intersect to equal 0 or not * * @return array of float */ diff --git a/src/PhpSpreadsheet/Calculation/TextData.php b/src/PhpSpreadsheet/Calculation/TextData.php index c7e91a47..0bde3b7f 100644 --- a/src/PhpSpreadsheet/Calculation/TextData.php +++ b/src/PhpSpreadsheet/Calculation/TextData.php @@ -399,8 +399,8 @@ class TextData * * @see Use the exact() method in the TextData\Text class instead * - * @param $value1 - * @param $value2 + * @param mixed $value1 + * @param mixed $value2 * * @return bool */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php index 846a3124..36b5efbd 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CaseConvert.php @@ -13,7 +13,7 @@ class CaseConvert * * Converts a string value to upper case. * - * @param mixed (string) $mixedCaseValue + * @param mixed $mixedCaseValue The string value to convert to lower case */ public static function lower($mixedCaseValue): string { @@ -31,7 +31,7 @@ class CaseConvert * * Converts a string value to upper case. * - * @param mixed (string) $mixedCaseValue + * @param mixed $mixedCaseValue The string value to convert to upper case */ public static function upper($mixedCaseValue): string { @@ -47,9 +47,9 @@ class CaseConvert /** * PROPERCASE. * - * Converts a string value to upper case. + * Converts a string value to proper or title case. * - * @param mixed (string) $mixedCaseValue + * @param mixed $mixedCaseValue The string value to convert to title case */ public static function proper($mixedCaseValue): string { diff --git a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php index 0003e0cd..4397b538 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php +++ b/src/PhpSpreadsheet/Calculation/TextData/CharacterConvert.php @@ -10,7 +10,7 @@ class CharacterConvert /** * CHARACTER. * - * @param mixed (int) $character Value + * @param mixed $character Integer Value to convert to its character representation */ public static function character($character): string { @@ -31,7 +31,7 @@ class CharacterConvert /** * ASCIICODE. * - * @param mixed (string) $characters Value + * @param mixed $characters String character to convert to its ASCII value * * @return int|string A string if arguments are invalid */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/Extract.php b/src/PhpSpreadsheet/Calculation/TextData/Extract.php index 7ef76546..2f994858 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Extract.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Extract.php @@ -10,8 +10,8 @@ class Extract /** * LEFT. * - * @param mixed (string) $value Value - * @param mixed (int) $chars Number of characters + * @param mixed $value String value from which to extract characters + * @param mixed $chars The number of characters to extract (as an integer) */ public static function left($value = '', $chars = 1): string { @@ -32,9 +32,9 @@ class Extract /** * MID. * - * @param mixed (string) $value Value - * @param mixed (int) $start Start character - * @param mixed (int) $chars Number of characters + * @param mixed $value String value from which to extract characters + * @param mixed $start Integer offset of the first character that we want to extract + * @param mixed $chars The number of characters to extract (as an integer) */ public static function mid($value = '', $start = 1, $chars = null): string { @@ -56,8 +56,8 @@ class Extract /** * RIGHT. * - * @param mixed (string) $value Value - * @param mixed (int) $chars Number of characters + * @param mixed $value String value from which to extract characters + * @param mixed $chars The number of characters to extract (as an integer) */ public static function right($value = '', $chars = 1): string { diff --git a/src/PhpSpreadsheet/Calculation/TextData/Format.php b/src/PhpSpreadsheet/Calculation/TextData/Format.php index c061818e..5c76454f 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Format.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Format.php @@ -18,10 +18,10 @@ class Format * 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 (float) $value The value to format - * @param mixed (int) $decimals The number of digits to display to the right of the decimal point. - * If decimals is negative, number is rounded to the left of the decimal point. - * If you omit decimals, it is assumed to be 2 + * @param mixed $value The value to format + * @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 */ public static function DOLLAR($value = 0, $decimals = 2): string { @@ -52,9 +52,9 @@ class Format /** * FIXEDFORMAT. * - * @param mixed $value Value to check - * @param mixed $decimals - * @param mixed (bool) $noCommas + * @param mixed $value The value to format + * @param mixed $decimals Integer value for the number of decimal places that should be formatted + * @param mixed $noCommas Boolean value indicating whether the value should have thousands separators or not */ public static function FIXEDFORMAT($value, $decimals = 2, $noCommas = false): string { @@ -87,8 +87,8 @@ class Format /** * TEXTFORMAT. * - * @param mixed $value Value to check - * @param mixed (string) $format Format mask to use + * @param mixed $value The value to format + * @param mixed $format A string with the Format mask that should be used */ public static function TEXTFORMAT($value, $format): string { @@ -151,9 +151,9 @@ class Format /** * NUMBERVALUE. * - * @param mixed $value Value to check - * @param mixed (string) $decimalSeparator decimal separator, defaults to locale defined value - * @param mixed (string) $groupSeparator group/thosands separator, defaults to locale defined value + * @param mixed $value The value to format + * @param mixed $decimalSeparator A string with the decimal separator to use, defaults to locale defined value + * @param mixed $groupSeparator A string with the group/thousands separator to use, defaults to locale defined value * * @return float|string */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/Replace.php b/src/PhpSpreadsheet/Calculation/TextData/Replace.php index a1975d6b..7ca710ef 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Replace.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Replace.php @@ -9,10 +9,10 @@ class Replace /** * REPLACE. * - * @param mixed (string) $oldText String to modify - * @param mixed (int) $start Start character - * @param mixed (int) $chars Number of characters - * @param mixed (string) $newText String to replace in defined position + * @param mixed $oldText The text string value to modify + * @param mixed $start Integer offset for start character of the replacement + * @param mixed $chars Integer number of characters to replace from the start offset + * @param mixed $newText String to replace in the defined position */ public static function replace($oldText, $start, $chars, $newText): string { @@ -30,10 +30,10 @@ class Replace /** * SUBSTITUTE. * - * @param mixed (string) $text Value - * @param mixed (string) $fromText From Value - * @param mixed (string) $toText To Value - * @param mixed (int) $instance Instance Number + * @param mixed $text The text string value to modify + * @param mixed $fromText The string value that we want to replace in $text + * @param mixed $toText The string value that we want to replace with in $text + * @param mixed $instance Integer instance Number for the occurrence of frmText to change */ public static function substitute($text = '', $fromText = '', $toText = '', $instance = 0): string { diff --git a/src/PhpSpreadsheet/Calculation/TextData/Search.php b/src/PhpSpreadsheet/Calculation/TextData/Search.php index cf1bf241..2da688d8 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Search.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Search.php @@ -11,9 +11,9 @@ class Search /** * SEARCHSENSITIVE. * - * @param mixed (string) $needle The string to look for - * @param mixed (string) $haystack The string in which to look - * @param mixed (int) $offset Offset within $haystack + * @param mixed $needle The string to look for + * @param mixed $haystack The string in which to look + * @param mixed $offset Integer offset within $haystack to start searching from * * @return int|string */ @@ -46,9 +46,9 @@ class Search /** * SEARCHINSENSITIVE. * - * @param mixed (string) $needle The string to look for - * @param mixed (string) $haystack The string in which to look - * @param mixed (int) $offset Offset within $haystack + * @param mixed $needle The string to look for + * @param mixed $haystack The string in which to look + * @param mixed $offset Integer offset within $haystack to start searching from * * @return int|string */ diff --git a/src/PhpSpreadsheet/Calculation/TextData/Text.php b/src/PhpSpreadsheet/Calculation/TextData/Text.php index 338cdd20..6e408891 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Text.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Text.php @@ -10,7 +10,7 @@ class Text /** * STRINGLENGTH. * - * @param mixed (string) $value Value + * @param mixed $value String Value */ public static function length($value = ''): int { @@ -28,8 +28,8 @@ class Text * EXACT is case-sensitive but ignores formatting differences. * Use EXACT to test text being entered into a document. * - * @param mixed (string) $value1 - * @param mixed (string) $value2 + * @param mixed $value1 String Value + * @param mixed $value2 String Value */ public static function exact($value1, $value2): bool { diff --git a/src/PhpSpreadsheet/Calculation/TextData/Trim.php b/src/PhpSpreadsheet/Calculation/TextData/Trim.php index 01fff1a8..b5d66455 100644 --- a/src/PhpSpreadsheet/Calculation/TextData/Trim.php +++ b/src/PhpSpreadsheet/Calculation/TextData/Trim.php @@ -12,7 +12,7 @@ class Trim /** * TRIMNONPRINTABLE. * - * @param mixed (string) $stringValue Value to check + * @param mixed $stringValue String Value to check * * @return null|string */ @@ -38,7 +38,7 @@ class Trim /** * TRIMSPACES. * - * @param mixed (string) $stringValue Value to check + * @param mixed $stringValue String Value to check * * @return null|string */ diff --git a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php index 337501f9..f1f0bea2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/CalculationTest.php @@ -319,8 +319,8 @@ class CalculationTest extends TestCase } /** - * @param $expectedResult - * @param $dataArray + * @param mixed $expectedResult + * @param mixed $dataArray * @param string $formula * @param string $cellCoordinates where to put the formula * @param string[] $shouldBeSetInCacheCells coordinates of cells that must diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php index f144c6f2..eceb0519 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/DateTime/TimeValueTest.php @@ -10,7 +10,7 @@ class TimeValueTest extends AllSetupTeardown * @dataProvider providerTIMEVALUE * * @param mixed $expectedResult - * @param $timeValue + * @param mixed $timeValue */ public function testTIMEVALUE($expectedResult, $timeValue): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfErrorTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfErrorTest.php index c1602eda..cf3a39d4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfErrorTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfErrorTest.php @@ -17,8 +17,8 @@ class IfErrorTest extends TestCase * @dataProvider providerIFERROR * * @param mixed $expectedResult - * @param $value - * @param $return + * @param mixed $value + * @param mixed $return */ public function testIFERROR($expectedResult, $value, $return): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfNaTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfNaTest.php index 2976761a..63302276 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfNaTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Logical/IfNaTest.php @@ -17,8 +17,8 @@ class IfNaTest extends TestCase * @dataProvider providerIFNA * * @param mixed $expectedResult - * @param $value - * @param $return + * @param mixed $value + * @param mixed $return */ public function testIFNA($expectedResult, $value, $return): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php index c8cc8645..56839f7f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/EvenTest.php @@ -8,7 +8,7 @@ class EvenTest extends AllSetupTeardown * @dataProvider providerEVEN * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testEVEN($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php index f3627205..83fe898c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/FactDoubleTest.php @@ -8,7 +8,7 @@ class FactDoubleTest extends AllSetupTeardown * @dataProvider providerFACTDOUBLE * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testFACTDOUBLE($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php index c599a30e..21740ef3 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/OddTest.php @@ -8,7 +8,7 @@ class OddTest extends AllSetupTeardown * @dataProvider providerODD * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testODD($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php index dff5370d..0ec90e78 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SignTest.php @@ -8,7 +8,7 @@ class SignTest extends AllSetupTeardown * @dataProvider providerSIGN * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testSIGN($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php index bb4bba4b..c49934ea 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php @@ -17,7 +17,7 @@ class SqrtPiTest extends TestCase * @dataProvider providerSQRTPI * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testSQRTPI($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherInvTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherInvTest.php index efd212c8..2c3e592c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherInvTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherInvTest.php @@ -17,7 +17,7 @@ class FisherInvTest extends TestCase * @dataProvider providerFISHERINV * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testFISHERINV($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherTest.php index 788ffc6a..7705517a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/FisherTest.php @@ -17,7 +17,7 @@ class FisherTest extends TestCase * @dataProvider providerFISHER * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testFISHER($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GammaLnTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GammaLnTest.php index d0ae623f..31407feb 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GammaLnTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Statistical/GammaLnTest.php @@ -17,7 +17,7 @@ class GammaLnTest extends TestCase * @dataProvider providerGAMMALN * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testGAMMALN($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CharTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CharTest.php index cf22df02..324f5054 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CharTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CharTest.php @@ -11,7 +11,7 @@ class CharTest extends TestCase * @dataProvider providerCHAR * * @param mixed $expectedResult - * @param $character + * @param mixed $character */ public function testCHAR($expectedResult, $character): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CleanTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CleanTest.php index 31dcc5e6..ddc27a5d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CleanTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CleanTest.php @@ -11,7 +11,7 @@ class CleanTest extends TestCase * @dataProvider providerCLEAN * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testCLEAN($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CodeTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CodeTest.php index 9c19f347..7bf5a00f 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CodeTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/CodeTest.php @@ -11,7 +11,7 @@ class CodeTest extends TestCase * @dataProvider providerCODE * * @param mixed $expectedResult - * @param $character + * @param mixed $character */ public function testCODE($expectedResult, $character): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LeftTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LeftTest.php index 080a9ac3..450643f7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LeftTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LeftTest.php @@ -33,7 +33,7 @@ class LeftTest extends TestCase * @dataProvider providerLocaleLEFT * * @param string $expectedResult - * @param $value + * @param mixed $value * @param mixed $locale * @param mixed $characters */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LenTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LenTest.php index bca2b389..c6ffc43d 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LenTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LenTest.php @@ -11,7 +11,7 @@ class LenTest extends TestCase * @dataProvider providerLEN * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testLEN($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LowerTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LowerTest.php index 085dd79f..f90ab378 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LowerTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/LowerTest.php @@ -17,7 +17,7 @@ class LowerTest extends TestCase * @dataProvider providerLOWER * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testLOWER($expectedResult, $value): void { @@ -34,7 +34,7 @@ class LowerTest extends TestCase * @dataProvider providerLocaleLOWER * * @param string $expectedResult - * @param $value + * @param mixed $value * @param mixed $locale */ public function testLowerWithLocaleBoolean($expectedResult, $locale, $value): void diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/MidTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/MidTest.php index 0d0678f2..c9859969 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/MidTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/MidTest.php @@ -33,7 +33,7 @@ class MidTest extends TestCase * @dataProvider providerLocaleMID * * @param string $expectedResult - * @param $value + * @param mixed $value * @param mixed $locale * @param mixed $offset * @param mixed $characters diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ProperTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ProperTest.php index 48359721..58096c5e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ProperTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ProperTest.php @@ -17,7 +17,7 @@ class ProperTest extends TestCase * @dataProvider providerPROPER * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testPROPER($expectedResult, $value): void { @@ -34,7 +34,7 @@ class ProperTest extends TestCase * @dataProvider providerLocaleLOWER * * @param string $expectedResult - * @param $value + * @param mixed $value * @param mixed $locale */ public function testLowerWithLocaleBoolean($expectedResult, $locale, $value): void diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php index da4c7491..26ccc549 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/RightTest.php @@ -33,7 +33,7 @@ class RightTest extends TestCase * @dataProvider providerLocaleRIGHT * * @param string $expectedResult - * @param $value + * @param mixed $value * @param mixed $locale * @param mixed $characters */ diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TTest.php index c7606c05..e2f5cd01 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TTest.php @@ -11,7 +11,7 @@ class TTest extends TestCase * @dataProvider providerT * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testT($expectedResult, $value): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TrimTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TrimTest.php index 91890ded..6b2dbc7a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TrimTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/TrimTest.php @@ -11,7 +11,7 @@ class TrimTest extends TestCase * @dataProvider providerTRIM * * @param mixed $expectedResult - * @param $character + * @param mixed $character */ public function testTRIM($expectedResult, $character): void { diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php index cf2d569d..352aa5b7 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/UpperTest.php @@ -17,7 +17,7 @@ class UpperTest extends TestCase * @dataProvider providerUPPER * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testUPPER($expectedResult, $value): void { @@ -34,7 +34,7 @@ class UpperTest extends TestCase * @dataProvider providerLocaleLOWER * * @param string $expectedResult - * @param $value + * @param mixed $value * @param mixed $locale */ public function testLowerWithLocaleBoolean($expectedResult, $locale, $value): void diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php index 355193de..607c926b 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/TextData/ValueTest.php @@ -32,7 +32,7 @@ class ValueTest extends TestCase * @dataProvider providerVALUE * * @param mixed $expectedResult - * @param $value + * @param mixed $value */ public function testVALUE($expectedResult, $value): void { From 6446039f4fce67ae851c8c6c3961357308c64f92 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sat, 3 Apr 2021 22:37:18 +0200 Subject: [PATCH 82/89] PHPStan stuff (#1984) --- .../Calculation/Calculation.php | 30 ++++++++++--------- src/PhpSpreadsheet/Style/Alignment.php | 3 +- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 0aa2a6da..1699b5a0 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -130,7 +130,7 @@ class Calculation /** * Error message for any error that was raised/thrown by the calculation engine. * - * @var string + * @var null|string */ public $formulaError; @@ -204,7 +204,7 @@ class Calculation /** * Locale-specific translations for Excel constants (True, False and Null). * - * @var string[] + * @var array */ public static $localeBoolean = [ 'TRUE' => 'TRUE', @@ -216,7 +216,7 @@ class Calculation * Excel constant string translations to their PHP equivalents * Constant conversion from text name/value to actual (datatyped) value. * - * @var string[] + * @var array */ private static $excelConstants = [ 'TRUE' => true, @@ -3457,8 +3457,8 @@ class Calculation /** * Ensure that paired matrix operands are both matrices and of the same size. * - * @param mixed &$operand1 First matrix operand - * @param mixed &$operand2 Second matrix operand + * @param mixed $operand1 First matrix operand + * @param mixed $operand2 Second matrix operand * @param int $resize Flag indicating whether the matrices should be resized to match * and (if so), whether the smaller dimension should grow or the * larger should shrink. @@ -3502,7 +3502,7 @@ class Calculation /** * Read the dimensions of a matrix, and re-index it with straight numeric keys starting from row 0, column 0. * - * @param array &$matrix matrix operand + * @param array $matrix matrix operand * * @return int[] An array comprising the number of rows, and number of columns */ @@ -3527,8 +3527,8 @@ class Calculation /** * Ensure that paired matrix operands are both matrices of the same size. * - * @param mixed &$matrix1 First matrix operand - * @param mixed &$matrix2 Second matrix operand + * @param mixed $matrix1 First matrix operand + * @param mixed $matrix2 Second matrix operand * @param int $matrix1Rows Row size of first matrix operand * @param int $matrix1Columns Column size of first matrix operand * @param int $matrix2Rows Row size of second matrix operand @@ -3570,8 +3570,8 @@ class Calculation /** * Ensure that paired matrix operands are both matrices of the same size. * - * @param mixed &$matrix1 First matrix operand - * @param mixed &$matrix2 Second matrix operand + * @param mixed $matrix1 First matrix operand + * @param mixed $matrix2 Second matrix operand * @param int $matrix1Rows Row size of first matrix operand * @param int $matrix1Columns Column size of first matrix operand * @param int $matrix2Rows Row size of second matrix operand @@ -3688,6 +3688,8 @@ class Calculation return $typeString . ' with a value of ' . $this->showValue($value); } + + return null; } /** @@ -3782,7 +3784,7 @@ class Calculation /** * @param string $formula * - * @return bool + * @return array|false */ private function internalParseFormula($formula, ?Cell $pCell = null) { @@ -4254,7 +4256,7 @@ class Calculation * @param mixed $tokens * @param null|string $cellID * - * @return bool + * @return array|false */ private function processTokenStack($tokens, $cellID = null, ?Cell $pCell = null) { @@ -5101,7 +5103,7 @@ class Calculation /** * Extract range values. * - * @param string &$pRange String based range representation + * @param string $pRange String based range representation * @param Worksheet $pSheet Worksheet * @param bool $resetLog Flag indicating whether calculation log should be reset or not * @@ -5154,7 +5156,7 @@ class Calculation /** * Extract range values. * - * @param string &$pRange String based range representation + * @param string $pRange String based range representation * @param Worksheet $pSheet Worksheet * @param bool $resetLog Flag indicating whether calculation log should be reset or not * diff --git a/src/PhpSpreadsheet/Style/Alignment.php b/src/PhpSpreadsheet/Style/Alignment.php index 04a089fe..226a5427 100644 --- a/src/PhpSpreadsheet/Style/Alignment.php +++ b/src/PhpSpreadsheet/Style/Alignment.php @@ -392,7 +392,8 @@ class Alignment extends Supervisor if ( $this->getHorizontal() != self::HORIZONTAL_GENERAL && $this->getHorizontal() != self::HORIZONTAL_LEFT && - $this->getHorizontal() != self::HORIZONTAL_RIGHT + $this->getHorizontal() != self::HORIZONTAL_RIGHT && + $this->getHorizontal() != self::HORIZONTAL_DISTRIBUTED ) { $pValue = 0; // indent not supported } From 42761f90b7b3ecd5e319cf330129a8b588abb2b5 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 4 Apr 2021 14:44:06 +0200 Subject: [PATCH 83/89] Financial start refactoring cash flow functions (#1986) * Start extracting CashFlow functions from Financial, beginning with the simple Single Rate flows * Extracting Variable Periodic and NonPeriodic CashFlow functions from Financial * Some more unit tests for exception cases --- src/PhpSpreadsheet/Calculation/Financial.php | 355 +++--------------- .../Calculation/Financial/CashFlow/Single.php | 104 +++++ .../CashFlow/Variable/NonPeriodic.php | 233 ++++++++++++ .../Financial/CashFlow/Variable/Periodic.php | 160 ++++++++ .../data/Calculation/Financial/FVSCHEDULE.php | 18 + tests/data/Calculation/Financial/IRR.php | 35 +- tests/data/Calculation/Financial/MIRR.php | 32 +- tests/data/Calculation/Financial/NPV.php | 8 +- .../data/Calculation/Financial/PDURATION.php | 36 +- tests/data/Calculation/Financial/RRI.php | 36 +- 10 files changed, 680 insertions(+), 337 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index 984d31bf..fde1d3da 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -756,21 +756,19 @@ class Financial * Excel Function: * FVSCHEDULE(principal,schedule) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Single::futureValue() + * Use the futureValue() method in the Financial\CashFlow\Single class instead + * * @param float $principal the present value * @param float[] $schedule an array of interest rates to apply * - * @return float + * @return float|string */ public static function FVSCHEDULE($principal, $schedule) { - $principal = Functions::flattenSingleValue($principal); - $schedule = Functions::flattenArray($schedule); - - foreach ($schedule as $rate) { - $principal *= 1 + $rate; - } - - return $principal; + return Financial\CashFlow\Single::futureValue($principal, $schedule); } /** @@ -876,6 +874,8 @@ class Financial * Excel Function: * IRR(values[,guess]) * + * @Deprecated 1.18.0 + * * @param mixed $values An array or a reference to cells that contain numbers for which you want * to calculate the internal rate of return. * Values must contain at least one positive value and one negative value to @@ -883,56 +883,13 @@ class Financial * @param mixed $guess A number that you guess is close to the result of IRR * * @return float|string + * + *@see Financial\CashFlow\Variable\Periodic::rate() + * Use the IRR() method in the Financial\CashFlow\Variable\Periodic class instead */ public static function IRR($values, $guess = 0.1) { - if (!is_array($values)) { - return Functions::VALUE(); - } - $values = Functions::flattenArray($values); - $guess = Functions::flattenSingleValue($guess); - - // create an initial range, with a root somewhere between 0 and guess - $x1 = 0.0; - $x2 = $guess; - $f1 = self::NPV($x1, $values); - $f2 = self::NPV($x2, $values); - for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { - if (($f1 * $f2) < 0.0) { - break; - } - if (abs($f1) < abs($f2)) { - $f1 = self::NPV($x1 += 1.6 * ($x1 - $x2), $values); - } else { - $f2 = self::NPV($x2 += 1.6 * ($x2 - $x1), $values); - } - } - if (($f1 * $f2) > 0.0) { - return Functions::VALUE(); - } - - $f = self::NPV($x1, $values); - if ($f < 0.0) { - $rtb = $x1; - $dx = $x2 - $x1; - } else { - $rtb = $x2; - $dx = $x1 - $x2; - } - - for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { - $dx *= 0.5; - $x_mid = $rtb + $dx; - $f_mid = self::NPV($x_mid, $values); - if ($f_mid <= 0.0) { - $rtb = $x_mid; - } - if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { - return $x_mid; - } - } - - return Functions::VALUE(); + return Financial\CashFlow\Variable\Periodic::rate($values, $guess); } /** @@ -986,6 +943,11 @@ class Financial * Excel Function: * MIRR(values,finance_rate, reinvestment_rate) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Variable\Periodic::modifiedRate() + * Use the MIRR() method in the Financial\CashFlow\Variable\Periodic class instead + * * @param mixed $values An array or a reference to cells that contain a series of payments and * income occurring at regular intervals. * Payments are negative value, income is positive values. @@ -996,34 +958,7 @@ class Financial */ public static function MIRR($values, $finance_rate, $reinvestment_rate) { - if (!is_array($values)) { - return Functions::VALUE(); - } - $values = Functions::flattenArray($values); - $finance_rate = Functions::flattenSingleValue($finance_rate); - $reinvestment_rate = Functions::flattenSingleValue($reinvestment_rate); - $n = count($values); - - $rr = 1.0 + $reinvestment_rate; - $fr = 1.0 + $finance_rate; - - $npv_pos = $npv_neg = 0.0; - foreach ($values as $i => $v) { - if ($v >= 0) { - $npv_pos += $v / $rr ** $i; - } else { - $npv_neg += $v / $fr ** $i; - } - } - - if (($npv_neg == 0) || ($npv_pos == 0) || ($reinvestment_rate <= -1)) { - return Functions::VALUE(); - } - - $mirr = ((-$npv_pos * $rr ** $n) - / ($npv_neg * ($rr))) ** (1.0 / ($n - 1)) - 1.0; - - return is_finite($mirr) ? $mirr : Functions::VALUE(); + return Financial\CashFlow\Variable\Periodic::modifiedRate($values, $finance_rate, $reinvestment_rate); } /** @@ -1094,28 +1029,16 @@ class Financial * * Returns the Net Present Value of a cash flow series given a discount rate. * + * @Deprecated 1.18.0 + * * @return float + * + *@see Financial\CashFlow\Variable\Periodic::presentValue() + * Use the NPV() method in the Financial\CashFlow\Variable\Periodic class instead */ public static function NPV(...$args) { - // Return value - $returnValue = 0; - - // Loop through arguments - $aArgs = Functions::flattenArray($args); - - // Calculate - $rate = array_shift($aArgs); - $countArgs = count($aArgs); - for ($i = 1; $i <= $countArgs; ++$i) { - // Is it a numeric value? - if (is_numeric($aArgs[$i - 1])) { - $returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i; - } - } - - // Return - return $returnValue; + return Financial\CashFlow\Variable\Periodic::presentValue(...$args); } /** @@ -1123,6 +1046,11 @@ class Financial * * Calculates the number of periods required for an investment to reach a specified value. * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Single::periods() + * Use the periods() method in the Financial\CashFlow\Single class instead + * * @param float $rate Interest rate per period * @param float $pv Present Value * @param float $fv Future Value @@ -1131,18 +1059,7 @@ class Financial */ public static function PDURATION($rate = 0, $pv = 0, $fv = 0) { - $rate = Functions::flattenSingleValue($rate); - $pv = Functions::flattenSingleValue($pv); - $fv = Functions::flattenSingleValue($fv); - - // Validate parameters - if (!is_numeric($rate) || !is_numeric($pv) || !is_numeric($fv)) { - return Functions::VALUE(); - } elseif ($rate <= 0.0 || $pv <= 0.0 || $fv <= 0.0) { - return Functions::NAN(); - } - - return (log($fv) - log($pv)) / log(1 + $rate); + return Financial\CashFlow\Single::periods($rate, $pv, $fv); } /** @@ -1470,6 +1387,11 @@ class Financial * * Calculates the interest rate required for an investment to grow to a specified future value . * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Single::interestRate() + * Use the interestRate() method in the Financial\CashFlow\Single class instead + * * @param float $nper The number of periods over which the investment is made * @param float $pv Present Value * @param float $fv Future Value @@ -1478,18 +1400,7 @@ class Financial */ public static function RRI($nper = 0, $pv = 0, $fv = 0) { - $nper = Functions::flattenSingleValue($nper); - $pv = Functions::flattenSingleValue($pv); - $fv = Functions::flattenSingleValue($fv); - - // Validate parameters - if (!is_numeric($nper) || !is_numeric($pv) || !is_numeric($fv)) { - return Functions::VALUE(); - } elseif ($nper <= 0.0 || $pv <= 0.0 || $fv < 0.0) { - return Functions::NAN(); - } - - return ($fv / $pv) ** (1 / $nper) - 1; + return Financial\CashFlow\Single::interestRate($nper, $pv, $fv); } /** @@ -1601,85 +1512,6 @@ class Financial return TreasuryBill::yield($settlement, $maturity, $price); } - private static function bothNegAndPos($neg, $pos) - { - return $neg && $pos; - } - - private static function xirrPart2(&$values) - { - $valCount = count($values); - $foundpos = false; - $foundneg = false; - for ($i = 0; $i < $valCount; ++$i) { - $fld = $values[$i]; - if (!is_numeric($fld)) { - return Functions::VALUE(); - } elseif ($fld > 0) { - $foundpos = true; - } elseif ($fld < 0) { - $foundneg = true; - } - } - if (!self::bothNegAndPos($foundneg, $foundpos)) { - return Functions::NAN(); - } - - return ''; - } - - private static function xirrPart1(&$values, &$dates) - { - if ((!is_array($values)) && (!is_array($dates))) { - return Functions::NA(); - } - $values = Functions::flattenArray($values); - $dates = Functions::flattenArray($dates); - if (count($values) != count($dates)) { - return Functions::NAN(); - } - - $datesCount = count($dates); - for ($i = 0; $i < $datesCount; ++$i) { - try { - $dates[$i] = DateTimeExcel\Helpers::getDateValue($dates[$i]); - } catch (Exception $e) { - return $e->getMessage(); - } - } - - return self::xirrPart2($values); - } - - private static function xirrPart3($values, $dates, $x1, $x2) - { - $f = self::xnpvOrdered($x1, $values, $dates, false); - if ($f < 0.0) { - $rtb = $x1; - $dx = $x2 - $x1; - } else { - $rtb = $x2; - $dx = $x1 - $x2; - } - - $rslt = Functions::VALUE(); - for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { - $dx *= 0.5; - $x_mid = $rtb + $dx; - $f_mid = self::xnpvOrdered($x_mid, $values, $dates, false); - if ($f_mid <= 0.0) { - $rtb = $x_mid; - } - if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { - $rslt = $x_mid; - - break; - } - } - - return $rslt; - } - /** * XIRR. * @@ -1688,6 +1520,11 @@ class Financial * Excel Function: * =XIRR(values,dates,guess) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Variable\NonPeriodic::rate() + * Use the rate() method in the Financial\CashFlow\Variable\NonPeriodic class instead + * * @param float[] $values A series of cash flow payments * The series of values must contain at least one positive value & one negative value * @param mixed[] $dates A series of payment dates @@ -1699,37 +1536,7 @@ class Financial */ public static function XIRR($values, $dates, $guess = 0.1) { - $rslt = self::xirrPart1($values, $dates); - if ($rslt) { - return $rslt; - } - - // create an initial range, with a root somewhere between 0 and guess - $guess = Functions::flattenSingleValue($guess); - $x1 = 0.0; - $x2 = $guess ?: 0.1; - $f1 = self::xnpvOrdered($x1, $values, $dates, false); - $f2 = self::xnpvOrdered($x2, $values, $dates, false); - $found = false; - for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { - if (!is_numeric($f1) || !is_numeric($f2)) { - break; - } - if (($f1 * $f2) < 0.0) { - $found = true; - - break; - } elseif (abs($f1) < abs($f2)) { - $f1 = self::xnpvOrdered($x1 += 1.6 * ($x1 - $x2), $values, $dates, false); - } else { - $f2 = self::xnpvOrdered($x2 += 1.6 * ($x2 - $x1), $values, $dates, false); - } - } - if (!$found) { - return Functions::NAN(); - } - - return self::xirrPart3($values, $dates, $x1, $x2); + return Financial\CashFlow\Variable\NonPeriodic::rate($values, $dates, $guess); } /** @@ -1741,6 +1548,11 @@ class Financial * Excel Function: * =XNPV(rate,values,dates) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Variable\NonPeriodic::presentValue() + * Use the presentValue() method in the Financial\CashFlow\Variable\NonPeriodic class instead + * * @param float $rate the discount rate to apply to the cash flows * @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates. * The first payment is optional and corresponds to a cost or payment that occurs at the beginning of the investment. @@ -1754,68 +1566,7 @@ class Financial */ public static function XNPV($rate, $values, $dates) { - return self::xnpvOrdered($rate, $values, $dates, true); - } - - private static function validateXnpv($rate, $values, $dates) - { - if (!is_numeric($rate)) { - return Functions::VALUE(); - } - $valCount = count($values); - if ($valCount != count($dates)) { - return Functions::NAN(); - } - if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) { - return Functions::NAN(); - } - $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); - if (is_string($date0)) { - return Functions::VALUE(); - } - - return ''; - } - - private static function xnpvOrdered($rate, $values, $dates, $ordered = true) - { - $rate = Functions::flattenSingleValue($rate); - $values = Functions::flattenArray($values); - $dates = Functions::flattenArray($dates); - $valCount = count($values); - - try { - $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); - } catch (Exception $e) { - return $e->getMessage(); - } - $rslt = self::validateXnpv($rate, $values, $dates); - if ($rslt) { - return $rslt; - } - $xnpv = 0.0; - for ($i = 0; $i < $valCount; ++$i) { - if (!is_numeric($values[$i])) { - return Functions::VALUE(); - } - - try { - $datei = DateTimeExcel\Helpers::getDateValue($dates[$i]); - } catch (Exception $e) { - return $e->getMessage(); - } - if ($date0 > $datei) { - $dif = $ordered ? Functions::NAN() : -DateTimeExcel\DateDif::funcDateDif($datei, $date0, 'd'); - } else { - $dif = DateTimeExcel\DateDif::funcDateDif($date0, $datei, 'd'); - } - if (!is_numeric($dif)) { - return $dif; - } - $xnpv += $values[$i] / (1 + $rate) ** ($dif / 365); - } - - return is_finite($xnpv) ? $xnpv : Functions::VALUE(); + return Financial\CashFlow\Variable\NonPeriodic::presentValue($rate, $values, $dates); } /** @@ -1823,7 +1574,10 @@ class Financial * * Returns the annual yield of a security that pays interest at maturity. * - * @see Use the yieldDiscounted() method in the Financial\Securities\Yields class instead + * @Deprecated 1.18.0 + * + * @see Financial\Securities\Yields::yieldDiscounted() + * Use the yieldDiscounted() method in the Financial\Securities\Yields class instead * * @param mixed $settlement The security's settlement date. * The security's settlement date is the date after the issue date when the security @@ -1853,7 +1607,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the yieldAtMaturity() method in the Financial\Securities\Yields class instead + * @see Financial\Securities\Yields::yieldAtMaturity() + * Use the yieldAtMaturity() method in the Financial\Securities\Yields class instead * * @param mixed $settlement The security's settlement date. * The security's settlement date is the date after the issue date when the security diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php index 3f1c8bc6..9fecd755 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Single.php @@ -2,6 +2,110 @@ namespace PhpOffice\PhpSpreadsheet\Calculation\Financial\CashFlow; +use PhpOffice\PhpSpreadsheet\Calculation\Exception; +use PhpOffice\PhpSpreadsheet\Calculation\Financial\BaseValidations; +use PhpOffice\PhpSpreadsheet\Calculation\Functions; + class Single { + use BaseValidations; + + /** + * FVSCHEDULE. + * + * Returns the future value of an initial principal after applying a series of compound interest rates. + * Use FVSCHEDULE to calculate the future value of an investment with a variable or adjustable rate. + * + * Excel Function: + * FVSCHEDULE(principal,schedule) + * + * @param mixed $principal the present value + * @param float[] $schedule an array of interest rates to apply + * + * @return float|string + */ + public static function futureValue($principal, $schedule) + { + $principal = Functions::flattenSingleValue($principal); + $schedule = Functions::flattenArray($schedule); + + try { + $principal = self::validateFloat($principal); + + foreach ($schedule as $rate) { + $rate = self::validateFloat($rate); + $principal *= 1 + $rate; + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $principal; + } + + /** + * PDURATION. + * + * Calculates the number of periods required for an investment to reach a specified value. + * + * @param float $rate Interest rate per period + * @param float $presentValue Present Value + * @param float $futureValue Future Value + * + * @return float|string Result, or a string containing an error + */ + public static function periods($rate = 0.0, $presentValue = 0.0, $futureValue = 0.0) + { + $rate = Functions::flattenSingleValue($rate); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = Functions::flattenSingleValue($futureValue); + + try { + $rate = self::validateFloat($rate); + $presentValue = self::validateFloat($presentValue); + $futureValue = self::validateFloat($futureValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($rate <= 0.0 || $presentValue <= 0.0 || $futureValue <= 0.0) { + return Functions::NAN(); + } + + return (log($futureValue) - log($presentValue)) / log(1 + $rate); + } + + /** + * RRI. + * + * Calculates the interest rate required for an investment to grow to a specified future value . + * + * @param float $periods The number of periods over which the investment is made + * @param float $presentValue Present Value + * @param float $futureValue Future Value + * + * @return float|string Result, or a string containing an error + */ + public static function interestRate($periods = 0.0, $presentValue = 0.0, $futureValue = 0.0) + { + $periods = Functions::flattenSingleValue($periods); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = Functions::flattenSingleValue($futureValue); + + try { + $periods = self::validateFloat($periods); + $presentValue = self::validateFloat($presentValue); + $futureValue = self::validateFloat($futureValue); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($periods <= 0.0 || $presentValue <= 0.0 || $futureValue < 0.0) { + return Functions::NAN(); + } + + return ($futureValue / $presentValue) ** (1 / $periods) - 1; + } } diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php new file mode 100644 index 00000000..d78015e6 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -0,0 +1,233 @@ +getMessage(); + } + } + + return self::xirrPart2($values); + } + + private static function xirrPart2(&$values) + { + $valCount = count($values); + $foundpos = false; + $foundneg = false; + for ($i = 0; $i < $valCount; ++$i) { + $fld = $values[$i]; + if (!is_numeric($fld)) { + return Functions::VALUE(); + } elseif ($fld > 0) { + $foundpos = true; + } elseif ($fld < 0) { + $foundneg = true; + } + } + if (!self::bothNegAndPos($foundneg, $foundpos)) { + return Functions::NAN(); + } + + return ''; + } + + private static function xirrPart3($values, $dates, $x1, $x2) + { + $f = self::xnpvOrdered($x1, $values, $dates, false); + if ($f < 0.0) { + $rtb = $x1; + $dx = $x2 - $x1; + } else { + $rtb = $x2; + $dx = $x1 - $x2; + } + + $rslt = Functions::VALUE(); + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + $dx *= 0.5; + $x_mid = $rtb + $dx; + $f_mid = self::xnpvOrdered($x_mid, $values, $dates, false); + if ($f_mid <= 0.0) { + $rtb = $x_mid; + } + if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { + $rslt = $x_mid; + + break; + } + } + + return $rslt; + } + + private static function xnpvOrdered($rate, $values, $dates, $ordered = true) + { + $rate = Functions::flattenSingleValue($rate); + $values = Functions::flattenArray($values); + $dates = Functions::flattenArray($dates); + $valCount = count($values); + + try { + $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); + } catch (Exception $e) { + return $e->getMessage(); + } + $rslt = self::validateXnpv($rate, $values, $dates); + if ($rslt) { + return $rslt; + } + $xnpv = 0.0; + for ($i = 0; $i < $valCount; ++$i) { + if (!is_numeric($values[$i])) { + return Functions::VALUE(); + } + + try { + $datei = DateTimeExcel\Helpers::getDateValue($dates[$i]); + } catch (Exception $e) { + return $e->getMessage(); + } + if ($date0 > $datei) { + $dif = $ordered ? Functions::NAN() : -DateTimeExcel\DateDif::funcDateDif($datei, $date0, 'd'); + } else { + $dif = DateTimeExcel\DateDif::funcDateDif($date0, $datei, 'd'); + } + if (!is_numeric($dif)) { + return $dif; + } + $xnpv += $values[$i] / (1 + $rate) ** ($dif / 365); + } + + return is_finite($xnpv) ? $xnpv : Functions::VALUE(); + } + + private static function validateXnpv($rate, $values, $dates) + { + if (!is_numeric($rate)) { + return Functions::VALUE(); + } + $valCount = count($values); + if ($valCount != count($dates)) { + return Functions::NAN(); + } + if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) { + return Functions::NAN(); + } + $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); + if (is_string($date0)) { + return Functions::VALUE(); + } + + return ''; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php new file mode 100644 index 00000000..c42df0c3 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/Periodic.php @@ -0,0 +1,160 @@ + 0.0) { + return Functions::VALUE(); + } + + $f = self::presentValue($x1, $values); + if ($f < 0.0) { + $rtb = $x1; + $dx = $x2 - $x1; + } else { + $rtb = $x2; + $dx = $x1 - $x2; + } + + for ($i = 0; $i < self::FINANCIAL_MAX_ITERATIONS; ++$i) { + $dx *= 0.5; + $x_mid = $rtb + $dx; + $f_mid = self::presentValue($x_mid, $values); + if ($f_mid <= 0.0) { + $rtb = $x_mid; + } + if ((abs($f_mid) < self::FINANCIAL_PRECISION) || (abs($dx) < self::FINANCIAL_PRECISION)) { + return $x_mid; + } + } + + return Functions::VALUE(); + } + + /** + * MIRR. + * + * Returns the modified internal rate of return for a series of periodic cash flows. MIRR considers both + * the cost of the investment and the interest received on reinvestment of cash. + * + * Excel Function: + * MIRR(values,finance_rate, reinvestment_rate) + * + * @param mixed $values An array or a reference to cells that contain a series of payments and + * income occurring at regular intervals. + * Payments are negative value, income is positive values. + * @param mixed $financeRate The interest rate you pay on the money used in the cash flows + * @param mixed $reinvestmentRate The interest rate you receive on the cash flows as you reinvest them + * + * @return float|string Result, or a string containing an error + */ + public static function modifiedRate($values, $financeRate, $reinvestmentRate) + { + if (!is_array($values)) { + return Functions::VALUE(); + } + $values = Functions::flattenArray($values); + $financeRate = Functions::flattenSingleValue($financeRate); + $reinvestmentRate = Functions::flattenSingleValue($reinvestmentRate); + $n = count($values); + + $rr = 1.0 + $reinvestmentRate; + $fr = 1.0 + $financeRate; + + $npvPos = $npvNeg = 0.0; + foreach ($values as $i => $v) { + if ($v >= 0) { + $npvPos += $v / $rr ** $i; + } else { + $npvNeg += $v / $fr ** $i; + } + } + + if (($npvNeg === 0.0) || ($npvPos === 0.0) || ($reinvestmentRate <= -1.0)) { + return Functions::VALUE(); + } + + $mirr = ((-$npvPos * $rr ** $n) + / ($npvNeg * ($rr))) ** (1.0 / ($n - 1)) - 1.0; + + return is_finite($mirr) ? $mirr : Functions::VALUE(); + } + + /** + * NPV. + * + * Returns the Net Present Value of a cash flow series given a discount rate. + * + * @param mixed $rate + * + * @return float + */ + public static function presentValue($rate, ...$args) + { + $returnValue = 0; + + $rate = Functions::flattenSingleValue($rate); + $aArgs = Functions::flattenArray($args); + + // Calculate + $countArgs = count($aArgs); + for ($i = 1; $i <= $countArgs; ++$i) { + // Is it a numeric value? + if (is_numeric($aArgs[$i - 1])) { + $returnValue += $aArgs[$i - 1] / (1 + $rate) ** $i; + } + } + + return $returnValue; + } +} diff --git a/tests/data/Calculation/Financial/FVSCHEDULE.php b/tests/data/Calculation/Financial/FVSCHEDULE.php index b332912c..975fca60 100644 --- a/tests/data/Calculation/Financial/FVSCHEDULE.php +++ b/tests/data/Calculation/Financial/FVSCHEDULE.php @@ -36,4 +36,22 @@ return [ ], ], ], + [ + '#VALUE!', + 'NaN', + [ + 0.089999999999999997, + 0.11, + 0.10000000000000001, + ], + ], + [ + '#VALUE!', + 100, + [ + 0.089999999999999997, + 'NaN', + 0.10000000000000001, + ], + ], ]; diff --git a/tests/data/Calculation/Financial/IRR.php b/tests/data/Calculation/Financial/IRR.php index 142ab204..f6c24c13 100644 --- a/tests/data/Calculation/Financial/IRR.php +++ b/tests/data/Calculation/Financial/IRR.php @@ -32,7 +32,7 @@ return [ 15000, 18000, ], - 0.10000000000000001, + 0.10, ], [ -0.13618951095869, @@ -41,7 +41,7 @@ return [ [ 20, 24, - 28.800000000000001, + 28.8, ], ], ], @@ -52,10 +52,35 @@ return [ [ 20, 24, - 28.800000000000001, - 34.560000000000002, - 41.469999999999999, + 28.8, + 34.56, + 41.47, ], ], ], + [ + '#VALUE!', + 999, + 1.23, + ], + [ + '#VALUE!', + [ + 70000, + 12000, + 15000, + 18000, + 21000, + ], + ], + [ + '#VALUE!', + [ + -70000, + -12000, + -15000, + -18000, + -21000, + ], + ], ]; diff --git a/tests/data/Calculation/Financial/MIRR.php b/tests/data/Calculation/Financial/MIRR.php index b32edcbe..9bd6e209 100644 --- a/tests/data/Calculation/Financial/MIRR.php +++ b/tests/data/Calculation/Financial/MIRR.php @@ -15,7 +15,7 @@ return [ 46000, ], ], - 0.10000000000000001, + 0.10, 0.12, ], [ @@ -28,7 +28,7 @@ return [ 21000, ], ], - 0.10000000000000001, + 0.10, 0.12, ], [ @@ -43,8 +43,8 @@ return [ 46000, ], ], - 0.10000000000000001, - 0.14000000000000001, + 0.10, + 0.14, ], [ 0.74021752686287001, @@ -74,4 +74,28 @@ return [ 5.5, 5, ], + [ + '#VALUE!', + 999, + 1.23, + 2.34, + ], + [ + '#VALUE!', + [0.12, 0.13, 0.125], + 1.23, + 2.34, + ], + [ + '#VALUE!', + [-0.12, -0.13, -0.125], + 1.23, + 2.34, + ], + [ + '#VALUE!', + [-0.12, 0.13, 0.125], + 1.23, + -2.34, + ], ]; diff --git a/tests/data/Calculation/Financial/NPV.php b/tests/data/Calculation/Financial/NPV.php index 27db7f4d..ac854269 100644 --- a/tests/data/Calculation/Financial/NPV.php +++ b/tests/data/Calculation/Financial/NPV.php @@ -5,7 +5,7 @@ return [ [ 1188.4434123352, - 0.10000000000000001, + 0.10, -10000, 3000, 4200, @@ -13,7 +13,7 @@ return [ ], [ 41922.061554931999, - 0.080000000000000002, + 0.08, 8000, 9200, 10000, @@ -22,7 +22,7 @@ return [ ], [ 36250.534912984003, - 0.080000000000000002, + 0.08, 8000, 9200, 10000, @@ -32,7 +32,7 @@ return [ ], [ 12678.677633095, - 0.050000000000000003, + 0.05, 2000, 2400, 2900, diff --git a/tests/data/Calculation/Financial/PDURATION.php b/tests/data/Calculation/Financial/PDURATION.php index cfb6080f..1886f26d 100644 --- a/tests/data/Calculation/Financial/PDURATION.php +++ b/tests/data/Calculation/Financial/PDURATION.php @@ -1,18 +1,6 @@ Date: Sat, 3 Apr 2021 17:42:11 +0900 Subject: [PATCH 84/89] PHPStan Level 2 --- .php_cs.dist | 2 +- phpstan.neon.dist | 7 +- src/PhpSpreadsheet/Calculation/DateTime.php | 2 +- .../Calculation/DateTimeExcel/DateDif.php | 2 +- .../Calculation/DateTimeExcel/DateValue.php | 2 +- .../Calculation/DateTimeExcel/Days360.php | 24 +- .../Calculation/DateTimeExcel/WeekNum.php | 4 +- .../Calculation/DateTimeExcel/WorkDay.php | 2 + .../Calculation/Engineering/BesselI.php | 8 +- .../Calculation/Engineering/BesselJ.php | 6 +- .../Calculation/Engineering/BesselK.php | 4 +- .../CashFlow/Variable/NonPeriodic.php | 1 + src/PhpSpreadsheet/Calculation/Functions.php | 4 +- .../Calculation/LookupRef/Matrix.php | 2 +- src/PhpSpreadsheet/Cell/AddressHelper.php | 4 +- src/PhpSpreadsheet/Cell/Coordinate.php | 19 +- src/PhpSpreadsheet/Chart/Axis.php | 6 +- src/PhpSpreadsheet/Chart/Chart.php | 2 +- src/PhpSpreadsheet/Chart/GridLines.php | 2 +- src/PhpSpreadsheet/Chart/PlotArea.php | 4 +- src/PhpSpreadsheet/Document/Properties.php | 6 +- src/PhpSpreadsheet/HashTable.php | 19 +- src/PhpSpreadsheet/Reader/Gnumeric.php | 4 +- src/PhpSpreadsheet/Reader/Html.php | 2 - src/PhpSpreadsheet/Reader/Ods.php | 11 +- src/PhpSpreadsheet/Reader/Slk.php | 6 +- src/PhpSpreadsheet/Reader/Xls.php | 11 +- src/PhpSpreadsheet/Reader/Xls/MD5.php | 27 +- src/PhpSpreadsheet/Reader/Xlsx.php | 20 +- src/PhpSpreadsheet/Reader/Xlsx/Chart.php | 6 +- src/PhpSpreadsheet/Reader/Xml.php | 13 +- src/PhpSpreadsheet/ReferenceHelper.php | 43 ++- src/PhpSpreadsheet/RichText/ITextElement.php | 2 +- src/PhpSpreadsheet/RichText/TextElement.php | 2 +- src/PhpSpreadsheet/Shared/Date.php | 4 +- src/PhpSpreadsheet/Shared/Font.php | 54 ++-- .../Shared/JAMA/CholeskyDecomposition.php | 4 +- src/PhpSpreadsheet/Shared/JAMA/Matrix.php | 11 - .../Shared/JAMA/QRDecomposition.php | 78 +++-- .../JAMA/SingularValueDecomposition.php | 2 +- .../Shared/OLE/ChainedBlockStream.php | 2 +- src/PhpSpreadsheet/Shared/OLE/PPS.php | 2 +- src/PhpSpreadsheet/Shared/OLE/PPS/Root.php | 4 +- src/PhpSpreadsheet/Shared/OLERead.php | 4 +- src/PhpSpreadsheet/Shared/StringHelper.php | 2 +- src/PhpSpreadsheet/Shared/Trend/Trend.php | 2 +- src/PhpSpreadsheet/Shared/Xls.php | 3 +- src/PhpSpreadsheet/Style/Border.php | 12 +- src/PhpSpreadsheet/Style/Color.php | 19 +- .../ConditionalFormatValueObject.php | 2 - .../ConditionalFormattingRuleExtension.php | 2 - src/PhpSpreadsheet/Style/Style.php | 47 ++- src/PhpSpreadsheet/Worksheet/AutoFilter.php | 2 +- src/PhpSpreadsheet/Worksheet/Worksheet.php | 14 +- src/PhpSpreadsheet/Writer/Html.php | 14 +- src/PhpSpreadsheet/Writer/Ods.php | 113 +++++-- src/PhpSpreadsheet/Writer/Ods/Content.php | 2 +- src/PhpSpreadsheet/Writer/Ods/Meta.php | 9 +- src/PhpSpreadsheet/Writer/Ods/MetaInf.php | 2 +- src/PhpSpreadsheet/Writer/Ods/Mimetype.php | 6 +- .../Writer/Ods/NamedExpressions.php | 4 +- src/PhpSpreadsheet/Writer/Ods/Settings.php | 7 +- src/PhpSpreadsheet/Writer/Ods/Styles.php | 5 +- src/PhpSpreadsheet/Writer/Ods/Thumbnails.php | 6 +- src/PhpSpreadsheet/Writer/Ods/WriterPart.php | 2 + src/PhpSpreadsheet/Writer/Xls.php | 13 +- src/PhpSpreadsheet/Writer/Xls/Escher.php | 8 +- src/PhpSpreadsheet/Writer/Xls/Parser.php | 24 +- src/PhpSpreadsheet/Writer/Xls/Workbook.php | 13 +- src/PhpSpreadsheet/Writer/Xls/Worksheet.php | 108 ++----- src/PhpSpreadsheet/Writer/Xlsx.php | 289 ++++++++++++------ src/PhpSpreadsheet/Writer/Xlsx/Chart.php | 8 +- src/PhpSpreadsheet/Writer/Xlsx/Comments.php | 5 +- src/PhpSpreadsheet/Writer/Xlsx/Drawing.php | 17 +- src/PhpSpreadsheet/Writer/Xlsx/Rels.php | 4 +- src/PhpSpreadsheet/Writer/Xlsx/Theme.php | 14 +- src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php | 4 +- .../Cell/AdvancedValueBinderTest.php | 3 - .../Cell/CoordinateTest.php | 14 + .../Cell/DefaultValueBinderTest.php | 3 +- tests/PhpSpreadsheetTests/DefinedNameTest.php | 1 + .../Functional/ColumnWidthTest.php | 2 - .../Functional/CommentsTest.php | 2 - .../Reader/CsvContiguousTest.php | 8 +- .../Reader/Security/XmlScannerTest.php | 2 - tests/PhpSpreadsheetTests/Reader/XlsxTest.php | 1 - .../Reader/Xml/XmlTest.php | 2 - tests/PhpSpreadsheetTests/SpreadsheetTest.php | 3 - tests/data/Cell/IndexesFromString.php | 74 +++++ tests/data/CellCoordinates.php | 28 ++ 90 files changed, 778 insertions(+), 591 deletions(-) create mode 100644 tests/data/Cell/IndexesFromString.php diff --git a/.php_cs.dist b/.php_cs.dist index f8797e88..1a646420 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -160,7 +160,7 @@ return PhpCsFixer\Config::create() 'php_unit_test_annotation' => true, 'php_unit_test_case_static_method_calls' => ['call_type' => 'self'], 'php_unit_test_class_requires_covers' => false, // We don't care as much as we should about coverage - 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_add_missing_param_annotation' => false, // Don't add things that bring no value 'phpdoc_align' => false, // Waste of time 'phpdoc_annotation_without_dot' => true, 'phpdoc_indent' => true, diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 476513dc..53bbb0e6 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -1,11 +1,16 @@ parameters: - level: 1 + level: 2 paths: - src/ - tests/ ignoreErrors: - '~^Class GdImage not found\.$~' + - '~^Return typehint of method .* has invalid type GdImage\.$~' + - '~^Property .* has unknown class GdImage as its type\.$~' + - '~^Parameter .* of method .* has invalid typehint type GdImage\.$~' # Ignore all JpGraph issues - '~^Constant (MARK_CIRCLE|MARK_CROSS|MARK_DIAMOND|MARK_DTRIANGLE|MARK_FILLEDCIRCLE|MARK_SQUARE|MARK_STAR|MARK_UTRIANGLE|MARK_X|SIDE_RIGHT) not found\.$~' - '~^Instantiated class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot) not found\.$~' + - '~^Call to method .*\(\) on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' + - '~^Access to property .* on an unknown class (AccBarPlot|AccLinePlot|BarPlot|ContourPlot|Graph|GroupBarPlot|GroupBarPlot|LinePlot|LinePlot|PieGraph|PiePlot|PiePlot3D|PiePlotC|RadarGraph|RadarPlot|ScatterPlot|Spline|StockPlot)\.$~' diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index e3580cde..3b79a6d6 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -198,7 +198,7 @@ class DateTime * @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 */ - public static function DATEVALUE($dateValue = 1) + public static function DATEVALUE($dateValue) { return DateTimeExcel\DateValue::funcDateValue($dateValue); } diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php index ace22cbf..c0d1fa4b 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateDif.php @@ -89,7 +89,7 @@ class DateDif private static function datedifM(DateInterval $PHPDiffDateObject): int { - return (int) 12 * $PHPDiffDateObject->format('%y') + $PHPDiffDateObject->format('%m'); + return 12 * (int) $PHPDiffDateObject->format('%y') + (int) $PHPDiffDateObject->format('%m'); } private static function datedifMD(int $startDays, int $endDays, DateTime $PHPEndDateObject, DateInterval $PHPDiffDateObject): int diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php index 3c15d06a..86b8d3d9 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/DateValue.php @@ -33,7 +33,7 @@ class DateValue * @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 */ - public static function funcDateValue($dateValue = 1) + public static function funcDateValue($dateValue) { $dti = new DateTimeImmutable(); $baseYear = Date::getExcelCalendar(); diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php index 18a1abc8..47de02c3 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Days360.php @@ -19,20 +19,20 @@ class Days360 * DAYS360(startDate,endDate[,method]) * * @param mixed $startDate Excel date serial value (float), PHP date timestamp (integer), - * PHP DateTime object, or a standard date string + * 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 + * PHP DateTime object, or a standard date string * @param 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 - * the starting date is earlier than the 30th of a month, the - * ending date becomes equal to the 1st of the next month; - * otherwise the ending date becomes equal to the 30th of the - * same month. - * 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. + * 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 + * the starting date is earlier than the 30th of a month, the + * ending date becomes equal to the 1st of the next month; + * otherwise the ending date becomes equal to the 30th of the + * same month. + * 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. * * @return int|string Number of days between start date and end date */ diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php index 1dd15edb..9b2de4d0 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WeekNum.php @@ -67,9 +67,9 @@ class WeekNum return 0; } Helpers::silly1900($PHPDateObject, '+ 5 years'); // 1905 calendar matches - $dayOfYear = $PHPDateObject->format('z'); + $dayOfYear = (int) $PHPDateObject->format('z'); $PHPDateObject->modify('-' . $dayOfYear . ' days'); - $firstDayOfFirstWeek = $PHPDateObject->format('w'); + $firstDayOfFirstWeek = (int) $PHPDateObject->format('w'); $daysInFirstWeek = (6 - $firstDayOfFirstWeek + $method) % 7; $daysInFirstWeek += 7 * !$daysInFirstWeek; $endFirstWeek = $daysInFirstWeek - 1; diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php index f812624e..09816d33 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/WorkDay.php @@ -129,6 +129,7 @@ class WorkDay $startDoW = WeekDay::funcWeekDay($startDate, 3); if (WeekDay::funcWeekDay($startDate, 3) >= 5) { + // @phpstan-ignore-next-line $startDate += -$startDoW + 4; ++$endDays; } @@ -173,6 +174,7 @@ class WorkDay // Adjust the calculated end date if it falls over a weekend $endDoW = WeekDay::funcWeekDay($endDate, 3); if ($endDoW >= 5) { + // @phpstan-ignore-next-line $endDate += -$endDoW + 4; } } diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php index 89977101..bbb24bd4 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselI.php @@ -22,11 +22,11 @@ class BesselI * This code provides a more accurate calculation * * @param mixed $x A float value at which to evaluate the function. - * If x is nonnumeric, BESSELI returns the #VALUE! error value. + * If x is nonnumeric, BESSELI returns the #VALUE! error value. * @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. + * 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. * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php index e16c1519..730e2870 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselJ.php @@ -21,11 +21,11 @@ class BesselJ * values with x < -8 and x > 8. This code provides a more accurate calculation * * @param mixed $x A float value at which to evaluate the function. - * If x is nonnumeric, BESSELJ returns the #VALUE! error value. + * If x is nonnumeric, BESSELJ returns the #VALUE! error value. * @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. + * 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 */ diff --git a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php index 57794e03..18a2ac5c 100644 --- a/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php +++ b/src/PhpSpreadsheet/Calculation/Engineering/BesselK.php @@ -19,10 +19,10 @@ class BesselK * BESSELK(x,ord) * * @param mixed $x A float value at which to evaluate the function. - * If x is nonnumeric, BESSELK returns the #VALUE! error value. + * If x is nonnumeric, BESSELK returns the #VALUE! error value. * @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 is nonnumeric, BESSELK returns the #VALUE! error value. * If $ord < 0, BESSELKI returns the #NUM! error value. * * @return float|string Result, or a string containing an error diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php index d78015e6..58f8fdaf 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -198,6 +198,7 @@ class NonPeriodic return $e->getMessage(); } if ($date0 > $datei) { + /** @phpstan-ignore-next-line */ $dif = $ordered ? Functions::NAN() : -DateTimeExcel\DateDif::funcDateDif($datei, $date0, 'd'); } else { $dif = DateTimeExcel\DateDif::funcDateDif($date0, $datei, 'd'); diff --git a/src/PhpSpreadsheet/Calculation/Functions.php b/src/PhpSpreadsheet/Calculation/Functions.php index 6ad387e8..34ebefed 100644 --- a/src/PhpSpreadsheet/Calculation/Functions.php +++ b/src/PhpSpreadsheet/Calculation/Functions.php @@ -576,7 +576,7 @@ class Functions /** * Convert a multi-dimensional array to a simple 1-dimensional array. * - * @param mixed (array) $array Array to be flattened + * @param array|mixed $array Array to be flattened * * @return array Flattened array */ @@ -609,7 +609,7 @@ class Functions /** * Convert a multi-dimensional array to a simple 1-dimensional array, but retain an element of indexing. * - * @param mixed (array) $array Array to be flattened + * @param array|mixed $array Array to be flattened * * @return array Flattened array */ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php index 59af4258..5b666608 100644 --- a/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php @@ -9,7 +9,7 @@ class Matrix /** * TRANSPOSE. * - * @param mixed (array) $matrixData A matrix of values + * @param array|mixed $matrixData A matrix of values * * @return array */ diff --git a/src/PhpSpreadsheet/Cell/AddressHelper.php b/src/PhpSpreadsheet/Cell/AddressHelper.php index 04fa3b8c..b0e34e25 100644 --- a/src/PhpSpreadsheet/Cell/AddressHelper.php +++ b/src/PhpSpreadsheet/Cell/AddressHelper.php @@ -27,7 +27,7 @@ class AddressHelper } // Bracketed R references are relative to the current row if ($rowReference[0] === '[') { - $rowReference = $currentRowNumber + trim($rowReference, '[]'); + $rowReference = $currentRowNumber + (int) trim($rowReference, '[]'); } $columnReference = $cellReference[4]; // Empty C reference is the current column @@ -36,7 +36,7 @@ class AddressHelper } // Bracketed C references are relative to the current column if (is_string($columnReference) && $columnReference[0] === '[') { - $columnReference = $currentColumnNumber + trim($columnReference, '[]'); + $columnReference = $currentColumnNumber + (int) trim($columnReference, '[]'); } if ($columnReference <= 0 || $rowReference <= 0) { diff --git a/src/PhpSpreadsheet/Cell/Coordinate.php b/src/PhpSpreadsheet/Cell/Coordinate.php index 8d81f3a1..0b3917f2 100644 --- a/src/PhpSpreadsheet/Cell/Coordinate.php +++ b/src/PhpSpreadsheet/Cell/Coordinate.php @@ -25,7 +25,7 @@ abstract class Coordinate * * @param string $pCoordinateString eg: 'A1' * - * @return string[] Array containing column and row (indexes 0 and 1) + * @return array{0: string, 1: string} Array containing column and row (indexes 0 and 1) */ public static function coordinateFromString($pCoordinateString) { @@ -40,6 +40,23 @@ abstract class Coordinate throw new Exception('Invalid cell coordinate ' . $pCoordinateString); } + /** + * Get indexes from a string coordinates. + * + * @param string $coordinates eg: 'A1', '$B$12' + * + * @return array{0: int, 1: int} Array containing column index and row index (indexes 0 and 1) + */ + public static function indexesFromString(string $coordinates): array + { + [$col, $row] = self::coordinateFromString($coordinates); + + return [ + self::columnIndexFromString(ltrim($col, '$')), + (int) ltrim($row, '$'), + ]; + } + /** * Checks if a coordinate represents a range of cells. * diff --git a/src/PhpSpreadsheet/Chart/Axis.php b/src/PhpSpreadsheet/Chart/Axis.php index 455a5faa..6a2e2df5 100644 --- a/src/PhpSpreadsheet/Chart/Axis.php +++ b/src/PhpSpreadsheet/Chart/Axis.php @@ -135,10 +135,8 @@ class Axis extends Properties * Get Series Data Type. * * @param mixed $format_code - * - * @return string */ - public function setAxisNumberProperties($format_code) + public function setAxisNumberProperties($format_code): void { $this->axisNumber['format'] = (string) $format_code; $this->axisNumber['source_linked'] = 0; @@ -367,7 +365,7 @@ class Axis extends Properties /** * Set Shadow Properties from Mapped Values. * - * @param mixed &$reference + * @param mixed $reference * * @return $this */ diff --git a/src/PhpSpreadsheet/Chart/Chart.php b/src/PhpSpreadsheet/Chart/Chart.php index 20eb2aee..4fdff6ff 100644 --- a/src/PhpSpreadsheet/Chart/Chart.php +++ b/src/PhpSpreadsheet/Chart/Chart.php @@ -424,7 +424,7 @@ class Chart /** * Get the top left position of the chart. * - * @return array an associative array containing the cell address, X-Offset and Y-Offset from the top left of that cell + * @return array{cell: string, xOffset: int, yOffset: int} an associative array containing the cell address, X-Offset and Y-Offset from the top left of that cell */ public function getTopLeftPosition() { diff --git a/src/PhpSpreadsheet/Chart/GridLines.php b/src/PhpSpreadsheet/Chart/GridLines.php index 385b278b..c388f2c9 100644 --- a/src/PhpSpreadsheet/Chart/GridLines.php +++ b/src/PhpSpreadsheet/Chart/GridLines.php @@ -318,7 +318,7 @@ class GridLines extends Properties /** * Set Shadow Properties Values. * - * @param mixed &$reference + * @param mixed $reference * * @return $this */ diff --git a/src/PhpSpreadsheet/Chart/PlotArea.php b/src/PhpSpreadsheet/Chart/PlotArea.php index 954777cf..fbd01184 100644 --- a/src/PhpSpreadsheet/Chart/PlotArea.php +++ b/src/PhpSpreadsheet/Chart/PlotArea.php @@ -43,10 +43,8 @@ class PlotArea /** * Get Number of Plot Groups. - * - * @return array of DataSeries */ - public function getPlotGroupCount() + public function getPlotGroupCount(): int { return count($this->plotSeries); } diff --git a/src/PhpSpreadsheet/Document/Properties.php b/src/PhpSpreadsheet/Document/Properties.php index 951d334d..911d53ce 100644 --- a/src/PhpSpreadsheet/Document/Properties.php +++ b/src/PhpSpreadsheet/Document/Properties.php @@ -392,13 +392,11 @@ class Properties /** * Get a Custom Property Type. * - * @return string + * @return null|string */ public function getCustomPropertyType(string $propertyName) { - if (isset($this->customProperties[$propertyName])) { - return $this->customProperties[$propertyName]['type']; - } + return $this->customProperties[$propertyName]['type'] ?? null; } private function identifyPropertyType($propertyValue) diff --git a/src/PhpSpreadsheet/HashTable.php b/src/PhpSpreadsheet/HashTable.php index 90ea806b..0823236c 100644 --- a/src/PhpSpreadsheet/HashTable.php +++ b/src/PhpSpreadsheet/HashTable.php @@ -2,12 +2,15 @@ namespace PhpOffice\PhpSpreadsheet; +/** + * @template T of IComparable + */ class HashTable { /** * HashTable elements. * - * @var IComparable[] + * @var T[] */ protected $items = []; @@ -21,7 +24,7 @@ class HashTable /** * Create a new \PhpOffice\PhpSpreadsheet\HashTable. * - * @param IComparable[] $pSource Optional source array to create HashTable from + * @param T[] $pSource Optional source array to create HashTable from */ public function __construct($pSource = null) { @@ -34,7 +37,7 @@ class HashTable /** * Add HashTable items from source. * - * @param IComparable[] $pSource Source array to create HashTable from + * @param T[] $pSource Source array to create HashTable from */ public function addFromSource(?array $pSource = null): void { @@ -51,7 +54,7 @@ class HashTable /** * Add HashTable item. * - * @param IComparable $pSource Item to add + * @param T $pSource Item to add */ public function add(IComparable $pSource): void { @@ -65,7 +68,7 @@ class HashTable /** * Remove HashTable item. * - * @param IComparable $pSource Item to remove + * @param T $pSource Item to remove */ public function remove(IComparable $pSource): void { @@ -123,7 +126,7 @@ class HashTable * * @param int $pIndex * - * @return IComparable + * @return T */ public function getByIndex($pIndex) { @@ -139,7 +142,7 @@ class HashTable * * @param string $pHashCode * - * @return IComparable + * @return T */ public function getByHashCode($pHashCode) { @@ -153,7 +156,7 @@ class HashTable /** * HashTable to array. * - * @return IComparable[] + * @return T[] */ public function toArray() { diff --git a/src/PhpSpreadsheet/Reader/Gnumeric.php b/src/PhpSpreadsheet/Reader/Gnumeric.php index 2bec2a13..dfba56d7 100644 --- a/src/PhpSpreadsheet/Reader/Gnumeric.php +++ b/src/PhpSpreadsheet/Reader/Gnumeric.php @@ -657,7 +657,7 @@ class Gnumeric extends BaseReader $column = $columnAttributes['No']; $columnWidth = ((float) $columnAttributes['Unit']) / 5.4; $hidden = (isset($columnAttributes['Hidden'])) && ((string) $columnAttributes['Hidden'] == '1'); - $columnCount = $columnAttributes['Count'] ?? 1; + $columnCount = (int) ($columnAttributes['Count'] ?? 1); while ($c < $column) { $this->spreadsheet->getActiveSheet()->getColumnDimension(Coordinate::stringFromColumnIndex($c + 1))->setWidth($defaultWidth); ++$c; @@ -696,7 +696,7 @@ class Gnumeric extends BaseReader $row = $rowAttributes['No']; $rowHeight = (float) $rowAttributes['Unit']; $hidden = (isset($rowAttributes['Hidden'])) && ((string) $rowAttributes['Hidden'] == '1'); - $rowCount = $rowAttributes['Count'] ?? 1; + $rowCount = (int) ($rowAttributes['Count'] ?? 1); while ($r < $row) { ++$r; $this->spreadsheet->getActiveSheet()->getRowDimension($r)->setRowHeight($defaultHeight); diff --git a/src/PhpSpreadsheet/Reader/Html.php b/src/PhpSpreadsheet/Reader/Html.php index 09148d9f..f235f9b1 100644 --- a/src/PhpSpreadsheet/Reader/Html.php +++ b/src/PhpSpreadsheet/Reader/Html.php @@ -910,8 +910,6 @@ class Html extends BaseReader /** * Check if has #, so we can get clean hex. * - * @param $value - * * @return null|string */ public function getStyleColor($value) diff --git a/src/PhpSpreadsheet/Reader/Ods.php b/src/PhpSpreadsheet/Reader/Ods.php index 59d934be..1a4d7ca3 100644 --- a/src/PhpSpreadsheet/Reader/Ods.php +++ b/src/PhpSpreadsheet/Reader/Ods.php @@ -380,9 +380,8 @@ class Ods extends BaseReader } $columnID = 'A'; - foreach ($childNode->childNodes as $key => $cellData) { - // @var \DOMElement $cellData - + /** @var DOMElement $cellData */ + foreach ($childNode->childNodes as $cellData) { if ($this->getReadFilter() !== null) { if (!$this->getReadFilter()->readCell($columnID, $rowID, $worksheetName)) { ++$columnID; @@ -672,8 +671,9 @@ class Ods extends BaseReader $this->lookForSelectedCells($settings, $spreadsheet, $configNs); } - private function lookForActiveSheet(DOMNode $settings, Spreadsheet $spreadsheet, string $configNs): void + private function lookForActiveSheet(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void { + /** @var DOMElement $t */ foreach ($settings->getElementsByTagNameNS($configNs, 'config-item') as $t) { if ($t->getAttributeNs($configNs, 'name') === 'ActiveTable') { try { @@ -687,8 +687,9 @@ class Ods extends BaseReader } } - private function lookForSelectedCells(DOMNode $settings, Spreadsheet $spreadsheet, string $configNs): void + private function lookForSelectedCells(DOMElement $settings, Spreadsheet $spreadsheet, string $configNs): void { + /** @var DOMElement $t */ foreach ($settings->getElementsByTagNameNS($configNs, 'config-item-map-named') as $t) { if ($t->getAttributeNs($configNs, 'name') === 'Tables') { foreach ($t->getElementsByTagNameNS($configNs, 'config-item-map-entry') as $ws) { diff --git a/src/PhpSpreadsheet/Reader/Slk.php b/src/PhpSpreadsheet/Reader/Slk.php index 89d80ffa..c7b6fc82 100644 --- a/src/PhpSpreadsheet/Reader/Slk.php +++ b/src/PhpSpreadsheet/Reader/Slk.php @@ -169,7 +169,7 @@ class Slk extends BaseReader foreach ($rowData as $rowDatum) { switch ($rowDatum[0]) { case 'X': - $columnIndex = substr($rowDatum, 1) - 1; + $columnIndex = (int) substr($rowDatum, 1) - 1; break; case 'Y': @@ -251,7 +251,7 @@ class Slk extends BaseReader } // Bracketed R references are relative to the current row if ($rowReference[0] == '[') { - $rowReference = $row + trim($rowReference, '[]'); + $rowReference = (int) $row + (int) trim($rowReference, '[]'); } $columnReference = $cellReference[4][0]; // Empty C reference is the current column @@ -260,7 +260,7 @@ class Slk extends BaseReader } // Bracketed C references are relative to the current column if ($columnReference[0] == '[') { - $columnReference = $column + trim($columnReference, '[]'); + $columnReference = (int) $column + (int) trim($columnReference, '[]'); } $A1CellReference = Coordinate::stringFromColumnIndex($columnReference) . $rowReference; diff --git a/src/PhpSpreadsheet/Reader/Xls.php b/src/PhpSpreadsheet/Reader/Xls.php index faa047da..35b55bc0 100644 --- a/src/PhpSpreadsheet/Reader/Xls.php +++ b/src/PhpSpreadsheet/Reader/Xls.php @@ -224,7 +224,7 @@ class Xls extends BaseReader /** * Shared fonts. * - * @var array + * @var Font[] */ private $objFonts; @@ -1293,7 +1293,7 @@ class Xls extends BaseReader } } // Named Value - // TODO Provide support for named values + // TODO Provide support for named values } } $this->data = null; @@ -3105,7 +3105,7 @@ class Xls extends BaseReader $len = min($charsLeft, $limitpos - $pos); for ($j = 0; $j < $len; ++$j) { $retstr .= $recordData[$pos + $j] - . chr(0); + . chr(0); } $charsLeft -= $len; $isCompressed = false; @@ -7191,6 +7191,7 @@ class Xls extends BaseReader { [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell); $baseCol = Coordinate::columnIndexFromString($baseCol) - 1; + $baseRow = (int) $baseRow; // offset: 0; size: 2; index to row (0... 65535) (or offset (-32768... 32767)) $rowIndex = self::getUInt2d($cellAddressStructure, 0); @@ -7368,8 +7369,8 @@ class Xls extends BaseReader */ private function readBIFF8CellRangeAddressB($subData, $baseCell = 'A1') { - [$baseCol, $baseRow] = Coordinate::coordinateFromString($baseCell); - $baseCol = Coordinate::columnIndexFromString($baseCol) - 1; + [$baseCol, $baseRow] = Coordinate::indexesFromString($baseCell); + $baseCol = $baseCol - 1; // TODO: if cell range is just a single cell, should this funciton // not just return e.g. 'A1' and not 'A1:A1' ? diff --git a/src/PhpSpreadsheet/Reader/Xls/MD5.php b/src/PhpSpreadsheet/Reader/Xls/MD5.php index c0417ba6..3e15f641 100644 --- a/src/PhpSpreadsheet/Reader/Xls/MD5.php +++ b/src/PhpSpreadsheet/Reader/Xls/MD5.php @@ -5,12 +5,25 @@ namespace PhpOffice\PhpSpreadsheet\Reader\Xls; class MD5 { // Context + + /** + * @var int + */ private $a; + /** + * @var int + */ private $b; + /** + * @var int + */ private $c; + /** + * @var int + */ private $d; /** @@ -56,7 +69,7 @@ class MD5 * * @param string $data Data to add */ - public function add($data): void + public function add(string $data): void { $words = array_values(unpack('V16', $data)); @@ -148,34 +161,34 @@ class MD5 $this->d = ($this->d + $D) & 0xffffffff; } - private static function f($X, $Y, $Z) + private static function f(int $X, int $Y, int $Z) { return ($X & $Y) | ((~$X) & $Z); // X AND Y OR NOT X AND Z } - private static function g($X, $Y, $Z) + private static function g(int $X, int $Y, int $Z) { return ($X & $Z) | ($Y & (~$Z)); // X AND Z OR Y AND NOT Z } - private static function h($X, $Y, $Z) + private static function h(int $X, int $Y, int $Z) { return $X ^ $Y ^ $Z; // X XOR Y XOR Z } - private static function i($X, $Y, $Z) + private static function i(int $X, int $Y, int $Z) { return $Y ^ ($X | (~$Z)); // Y XOR (X OR NOT Z) } - private static function step($func, &$A, $B, $C, $D, $M, $s, $t): void + private static function step($func, int &$A, int $B, int $C, int $D, int $M, int $s, int $t): void { $A = ($A + call_user_func($func, $B, $C, $D) + $M + $t) & 0xffffffff; $A = self::rotate($A, $s); $A = ($B + $A) & 0xffffffff; } - private static function rotate($decimal, $bits) + private static function rotate(int $decimal, int $bits) { $binary = str_pad(decbin($decimal), 32, '0', STR_PAD_LEFT); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index e47ad7b0..85b6c174 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -272,11 +272,11 @@ class Xlsx extends BaseReader if (!isset($sharedFormulas[(string) $c->f['si']])) { $sharedFormulas[$instance] = ['master' => $r, 'formula' => $value]; } else { - $master = Coordinate::coordinateFromString($sharedFormulas[$instance]['master']); - $current = Coordinate::coordinateFromString($r); + $master = Coordinate::indexesFromString($sharedFormulas[$instance]['master']); + $current = Coordinate::indexesFromString($r); $difference = [0, 0]; - $difference[0] = Coordinate::columnIndexFromString($current[0]) - Coordinate::columnIndexFromString($master[0]); + $difference[0] = $current[0] - $master[0]; $difference[1] = $current[1] - $master[1]; $value = $this->referenceHelper->updateFormulaReferences($sharedFormulas[$instance]['formula'], 'A1', $difference[0], $difference[1]); @@ -1141,7 +1141,7 @@ class Xlsx extends BaseReader )], false ); - $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((string) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1)); + $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1)); $objDrawing->setOffsetX(Drawing::EMUToPixels($oneCellAnchor->from->colOff)); $objDrawing->setOffsetY(Drawing::EMUToPixels($oneCellAnchor->from->rowOff)); $objDrawing->setResizeProportional(false); @@ -1167,7 +1167,7 @@ class Xlsx extends BaseReader $objDrawing->setWorksheet($docSheet); } elseif ($this->includeCharts && $oneCellAnchor->graphicFrame) { // Exported XLSX from Google Sheets positions charts with a oneCellAnchor - $coordinates = Coordinate::stringFromColumnIndex(((string) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1); + $coordinates = Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1); $offsetX = Drawing::EMUToPixels($oneCellAnchor->from->colOff); $offsetY = Drawing::EMUToPixels($oneCellAnchor->from->rowOff); $width = Drawing::EMUToPixels(self::getArrayItem($oneCellAnchor->ext->attributes(), 'cx')); @@ -1207,7 +1207,7 @@ class Xlsx extends BaseReader )], false ); - $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1)); + $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1)); $objDrawing->setOffsetX(Drawing::EMUToPixels($twoCellAnchor->from->colOff)); $objDrawing->setOffsetY(Drawing::EMUToPixels($twoCellAnchor->from->rowOff)); $objDrawing->setResizeProportional(false); @@ -1233,10 +1233,10 @@ class Xlsx extends BaseReader $objDrawing->setWorksheet($docSheet); } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) { - $fromCoordinate = Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1); + $fromCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1); $fromOffsetX = Drawing::EMUToPixels($twoCellAnchor->from->colOff); $fromOffsetY = Drawing::EMUToPixels($twoCellAnchor->from->rowOff); - $toCoordinate = Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1); + $toCoordinate = Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->to->col) + 1) . ($twoCellAnchor->to->row + 1); $toOffsetX = Drawing::EMUToPixels($twoCellAnchor->to->colOff); $toOffsetY = Drawing::EMUToPixels($twoCellAnchor->to->rowOff); $graphic = $twoCellAnchor->graphicFrame->children('http://schemas.openxmlformats.org/drawingml/2006/main')->graphic; @@ -1728,7 +1728,7 @@ class Xlsx extends BaseReader * * @return RichText */ - private function parseRichText($is) + private function parseRichText(?SimpleXMLElement $is) { $value = new RichText(); @@ -1736,6 +1736,8 @@ 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)); diff --git a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php index 5e86c60a..c9fc2f66 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Reader/Xlsx/Chart.php @@ -90,7 +90,7 @@ class Chart break; case 'valAx': - if (isset($chartDetail->title)) { + if (isset($chartDetail->title, $chartDetail->axPos)) { $axisLabel = self::chartTitle($chartDetail->title->children($namespacesChartMeta['c']), $namespacesChartMeta); $axPos = self::getAttribute($chartDetail->axPos, 'val', 'string'); @@ -355,7 +355,7 @@ class Chart } elseif (isset($seriesDetail->numRef)) { $seriesSource = (string) $seriesDetail->numRef->f; $seriesValues = new DataSeriesValues(DataSeriesValues::DATASERIES_TYPE_NUMBER, $seriesSource, null, null, null, $marker); - if (isset($seriesDetail->strRef->strCache)) { + if (isset($seriesDetail->numRef->numCache)) { $seriesData = self::chartDataSeriesValues($seriesDetail->numRef->numCache->children($namespacesChartMeta['c'])); $seriesValues ->setFormatCode($seriesData['formatCode']) @@ -539,7 +539,7 @@ class Chart { $plotAttributes = []; if (isset($chartDetail->dLbls)) { - if (isset($chartDetail->dLbls->howLegendKey)) { + if (isset($chartDetail->dLbls->showLegendKey)) { $plotAttributes['showLegendKey'] = self::getAttribute($chartDetail->dLbls->showLegendKey, 'val', 'string'); } if (isset($chartDetail->dLbls->showVal)) { diff --git a/src/PhpSpreadsheet/Reader/Xml.php b/src/PhpSpreadsheet/Reader/Xml.php index 58d38b0d..a900ad9b 100644 --- a/src/PhpSpreadsheet/Reader/Xml.php +++ b/src/PhpSpreadsheet/Reader/Xml.php @@ -422,6 +422,7 @@ class Xml extends BaseReader $worksheetID = 0; $xml_ss = $xml->children($namespaces['ss']); + /** @var null|SimpleXMLElement $worksheetx */ foreach ($xml_ss->Worksheet as $worksheetx) { $worksheet = $worksheetx ?? new SimpleXMLElement(''); $worksheet_ss = self::getAttributes($worksheet, $namespaces['ss']); @@ -748,9 +749,6 @@ class Xml extends BaseReader private static $borderPositions = ['top', 'left', 'bottom', 'right']; - /** - * @param $styleID - */ private function parseStyleBorders($styleID, SimpleXMLElement $styleData, array $namespaces): void { $diagonalDirection = ''; @@ -821,9 +819,6 @@ class Xml extends BaseReader } } - /** - * @param $styleID - */ private function parseStyleFont(string $styleID, SimpleXMLElement $styleAttributes): void { foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValue) { @@ -861,9 +856,6 @@ class Xml extends BaseReader } } - /** - * @param $styleID - */ private function parseStyleInterior($styleID, SimpleXMLElement $styleAttributes): void { foreach ($styleAttributes as $styleAttributeKey => $styleAttributeValuex) { @@ -887,9 +879,6 @@ class Xml extends BaseReader } } - /** - * @param $styleID - */ private function parseStyleNumberFormat($styleID, SimpleXMLElement $styleAttributes): void { $fromFormats = ['\-', '\ ']; diff --git a/src/PhpSpreadsheet/ReferenceHelper.php b/src/PhpSpreadsheet/ReferenceHelper.php index 513f0a53..d4fced37 100644 --- a/src/PhpSpreadsheet/ReferenceHelper.php +++ b/src/PhpSpreadsheet/ReferenceHelper.php @@ -375,17 +375,16 @@ class ReferenceHelper $allCoordinates = $pSheet->getCoordinates(); // Get coordinate of $pBefore - [$beforeColumn, $beforeRow] = Coordinate::coordinateFromString($pBefore); - $beforeColumnIndex = Coordinate::columnIndexFromString($beforeColumn); + [$beforeColumn, $beforeRow] = Coordinate::indexesFromString($pBefore); // Clear cells if we are removing columns or rows $highestColumn = $pSheet->getHighestColumn(); $highestRow = $pSheet->getHighestRow(); // 1. Clear column strips if we are removing columns - if ($pNumCols < 0 && $beforeColumnIndex - 2 + $pNumCols > 0) { + if ($pNumCols < 0 && $beforeColumn - 2 + $pNumCols > 0) { for ($i = 1; $i <= $highestRow - 1; ++$i) { - for ($j = $beforeColumnIndex - 1 + $pNumCols; $j <= $beforeColumnIndex - 2; ++$j) { + for ($j = $beforeColumn - 1 + $pNumCols; $j <= $beforeColumn - 2; ++$j) { $coordinate = Coordinate::stringFromColumnIndex($j + 1) . $i; $pSheet->removeConditionalStyles($coordinate); if ($pSheet->cellExists($coordinate)) { @@ -398,7 +397,7 @@ class ReferenceHelper // 2. Clear row strips if we are removing rows if ($pNumRows < 0 && $beforeRow - 1 + $pNumRows > 0) { - for ($i = $beforeColumnIndex - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$i) { + for ($i = $beforeColumn - 1; $i <= Coordinate::columnIndexFromString($highestColumn) - 1; ++$i) { for ($j = $beforeRow + $pNumRows; $j <= $beforeRow - 1; ++$j) { $coordinate = Coordinate::stringFromColumnIndex($i + 1) . $j; $pSheet->removeConditionalStyles($coordinate); @@ -427,7 +426,7 @@ class ReferenceHelper $newCoordinate = Coordinate::stringFromColumnIndex($cellIndex + $pNumCols) . ($cell->getRow() + $pNumRows); // Should the cell be updated? Move value and cellXf index from one cell to another. - if (($cellIndex >= $beforeColumnIndex) && ($cell->getRow() >= $beforeRow)) { + if (($cellIndex >= $beforeColumn) && ($cell->getRow() >= $beforeRow)) { // Update cell styles $pSheet->getCell($newCoordinate)->setXfIndex($cell->getXfIndex()); @@ -457,15 +456,15 @@ class ReferenceHelper $highestColumn = $pSheet->getHighestColumn(); $highestRow = $pSheet->getHighestRow(); - if ($pNumCols > 0 && $beforeColumnIndex - 2 > 0) { + if ($pNumCols > 0 && $beforeColumn - 2 > 0) { for ($i = $beforeRow; $i <= $highestRow - 1; ++$i) { // Style - $coordinate = Coordinate::stringFromColumnIndex($beforeColumnIndex - 1) . $i; + $coordinate = Coordinate::stringFromColumnIndex($beforeColumn - 1) . $i; if ($pSheet->cellExists($coordinate)) { $xfIndex = $pSheet->getCell($coordinate)->getXfIndex(); $conditionalStyles = $pSheet->conditionalStylesExists($coordinate) ? $pSheet->getConditionalStyles($coordinate) : false; - for ($j = $beforeColumnIndex; $j <= $beforeColumnIndex - 1 + $pNumCols; ++$j) { + for ($j = $beforeColumn; $j <= $beforeColumn - 1 + $pNumCols; ++$j) { $pSheet->getCellByColumnAndRow($j, $i)->setXfIndex($xfIndex); if ($conditionalStyles) { $cloned = []; @@ -480,7 +479,7 @@ class ReferenceHelper } if ($pNumRows > 0 && $beforeRow - 1 > 0) { - for ($i = $beforeColumnIndex; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) { + for ($i = $beforeColumn; $i <= Coordinate::columnIndexFromString($highestColumn); ++$i) { // Style $coordinate = Coordinate::stringFromColumnIndex($i) . ($beforeRow - 1); if ($pSheet->cellExists($coordinate)) { @@ -502,28 +501,28 @@ class ReferenceHelper } // Update worksheet: column dimensions - $this->adjustColumnDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustColumnDimensions($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: row dimensions - $this->adjustRowDimensions($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustRowDimensions($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: page breaks - $this->adjustPageBreaks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustPageBreaks($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: comments - $this->adjustComments($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustComments($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: hyperlinks - $this->adjustHyperlinks($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustHyperlinks($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: data validations - $this->adjustDataValidations($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustDataValidations($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: merge cells - $this->adjustMergeCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustMergeCells($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: protected cells - $this->adjustProtectedCells($pSheet, $pBefore, $beforeColumnIndex, $pNumCols, $beforeRow, $pNumRows); + $this->adjustProtectedCells($pSheet, $pBefore, $beforeColumn, $pNumCols, $beforeRow, $pNumRows); // Update worksheet: autofilter $autoFilter = $pSheet->getAutoFilter(); @@ -654,7 +653,7 @@ class ReferenceHelper $toString .= $modified3 . ':' . $modified4; // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more $column = 100000; - $row = 10000000 + trim($match[3], '$'); + $row = 10000000 + (int) trim($match[3], '$'); $cellIndex = $column . $row; $newCellTokens[$cellIndex] = preg_quote($toString, '/'); @@ -705,7 +704,7 @@ class ReferenceHelper [$column, $row] = Coordinate::coordinateFromString($match[3]); // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; - $row = trim($row, '$') + 10000000; + $row = (int) trim($row, '$') + 10000000; $cellIndex = $column . $row; $newCellTokens[$cellIndex] = preg_quote($toString, '/'); @@ -731,7 +730,7 @@ class ReferenceHelper [$column, $row] = Coordinate::coordinateFromString($match[3]); // Max worksheet size is 1,048,576 rows by 16,384 columns in Excel 2007, so our adjustments need to be at least one digit more $column = Coordinate::columnIndexFromString(trim($column, '$')) + 100000; - $row = trim($row, '$') + 10000000; + $row = (int) trim($row, '$') + 10000000; $cellIndex = $row . $column; $newCellTokens[$cellIndex] = preg_quote($toString, '/'); @@ -1021,7 +1020,7 @@ class ReferenceHelper // Create new row reference if ($updateRow) { - $newRow = $newRow + $pNumRows; + $newRow = (int) $newRow + $pNumRows; } // Return new reference diff --git a/src/PhpSpreadsheet/RichText/ITextElement.php b/src/PhpSpreadsheet/RichText/ITextElement.php index 69954676..39b70c86 100644 --- a/src/PhpSpreadsheet/RichText/ITextElement.php +++ b/src/PhpSpreadsheet/RichText/ITextElement.php @@ -14,7 +14,7 @@ interface ITextElement /** * Set text. * - * @param $text string Text + * @param string $text Text * * @return ITextElement */ diff --git a/src/PhpSpreadsheet/RichText/TextElement.php b/src/PhpSpreadsheet/RichText/TextElement.php index f8be5d55..26aebc0e 100644 --- a/src/PhpSpreadsheet/RichText/TextElement.php +++ b/src/PhpSpreadsheet/RichText/TextElement.php @@ -35,7 +35,7 @@ class TextElement implements ITextElement /** * Set text. * - * @param $text string Text + * @param string $text Text * * @return $this */ diff --git a/src/PhpSpreadsheet/Shared/Date.php b/src/PhpSpreadsheet/Shared/Date.php index a6cacb6f..49b3425c 100644 --- a/src/PhpSpreadsheet/Shared/Date.php +++ b/src/PhpSpreadsheet/Shared/Date.php @@ -304,8 +304,8 @@ class Date } // Calculate the Julian Date, then subtract the Excel base date (JD 2415020 = 31-Dec-1899 Giving Excel Date of 0) - $century = substr($year, 0, 2); - $decade = substr($year, 2, 2); + $century = (int) substr($year, 0, 2); + $decade = (int) substr($year, 2, 2); $excelDate = floor((146097 * $century) / 4) + floor((1461 * $decade) / 4) + floor((153 * $month + 2) / 5) + $day + 1721119 - $myexcelBaseDate + $excel1900isLeapYear; $excelTime = (($hours * 3600) + ($minutes * 60) + $seconds) / 86400; diff --git a/src/PhpSpreadsheet/Shared/Font.php b/src/PhpSpreadsheet/Shared/Font.php index 4061b370..94e35dfe 100644 --- a/src/PhpSpreadsheet/Shared/Font.php +++ b/src/PhpSpreadsheet/Shared/Font.php @@ -273,14 +273,8 @@ class Font /** * Get GD text width in pixels for a string of text in a certain font at a certain rotation angle. - * - * @param string $text - * @param \PhpOffice\PhpSpreadsheet\Style\Font - * @param int $rotation - * - * @return int */ - public static function getTextWidthPixelsExact($text, \PhpOffice\PhpSpreadsheet\Style\Font $font, $rotation = 0) + public static function getTextWidthPixelsExact(string $text, \PhpOffice\PhpSpreadsheet\Style\Font $font, int $rotation = 0): int { if (!function_exists('imagettfbbox')) { throw new PhpSpreadsheetException('GD library needs to be enabled'); @@ -350,7 +344,7 @@ class Font } else { // rotated text $columnWidth = $columnWidth * cos(deg2rad($rotation)) - + $fontSize * abs(sin(deg2rad($rotation))) / 5; // approximation + + $fontSize * abs(sin(deg2rad($rotation))) / 5; // approximation } } @@ -415,35 +409,35 @@ class Font switch ($name) { case 'Arial': $fontFile = ( - $bold ? ($italic ? self::ARIAL_BOLD_ITALIC : self::ARIAL_BOLD) - : ($italic ? self::ARIAL_ITALIC : self::ARIAL) + $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) + $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) + $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 + $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) + $bold ? ($italic ? self::GEORGIA_BOLD_ITALIC : self::GEORGIA_BOLD) + : ($italic ? self::GEORGIA_ITALIC : self::GEORGIA) ); break; @@ -453,8 +447,8 @@ class Font break; case 'Liberation Sans': $fontFile = ( - $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) - : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) + $bold ? ($italic ? self::LIBERATION_SANS_BOLD_ITALIC : self::LIBERATION_SANS_BOLD) + : ($italic ? self::LIBERATION_SANS_ITALIC : self::LIBERATION_SANS) ); break; @@ -472,8 +466,8 @@ class Font break; case 'Palatino Linotype': $fontFile = ( - $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) - : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) + $bold ? ($italic ? self::PALATINO_LINOTYPE_BOLD_ITALIC : self::PALATINO_LINOTYPE_BOLD) + : ($italic ? self::PALATINO_LINOTYPE_ITALIC : self::PALATINO_LINOTYPE) ); break; @@ -483,28 +477,28 @@ class Font break; case 'Tahoma': $fontFile = ( - $bold ? self::TAHOMA_BOLD : self::TAHOMA + $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) + $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) + $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) + $bold ? ($italic ? self::VERDANA_BOLD_ITALIC : self::VERDANA_BOLD) + : ($italic ? self::VERDANA_ITALIC : self::VERDANA) ); break; @@ -563,13 +557,13 @@ class Font // Exact width can be determined $columnWidth = $pPixels ? self::$defaultColumnWidths[$font->getName()][$font->getSize()]['px'] - : self::$defaultColumnWidths[$font->getName()][$font->getSize()]['width']; + : self::$defaultColumnWidths[$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 = $pPixels ? self::$defaultColumnWidths['Calibri'][11]['px'] - : self::$defaultColumnWidths['Calibri'][11]['width']; + : self::$defaultColumnWidths['Calibri'][11]['width']; $columnWidth = $columnWidth * $font->getSize() / 11; // Round pixels to closest integer diff --git a/src/PhpSpreadsheet/Shared/JAMA/CholeskyDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/CholeskyDecomposition.php index 2b241d55..27d02176 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/CholeskyDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/CholeskyDecomposition.php @@ -103,7 +103,7 @@ class CholeskyDecomposition /** * Solve A*X = B. * - * @param $B Row-equal matrix + * @param Matrix $B Row-equal matrix * * @return Matrix L * L' * X = B */ @@ -111,7 +111,7 @@ class CholeskyDecomposition { if ($B->getRowDimension() == $this->m) { if ($this->isspd) { - $X = $B->getArrayCopy(); + $X = $B->getArray(); $nx = $B->getColumnDimension(); for ($k = 0; $k < $this->m; ++$k) { diff --git a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php index 5182993c..9cbc9530 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/Matrix.php +++ b/src/PhpSpreadsheet/Shared/JAMA/Matrix.php @@ -456,17 +456,6 @@ class Matrix return $s; } - /** - * uminus. - * - * Unary minus matrix -A - * - * @return Matrix Unary minus matrix - */ - public function uminus() - { - } - /** * plus. * diff --git a/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php index 027706c1..9b51f413 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/QRDecomposition.php @@ -15,9 +15,9 @@ use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalculationException; * of simultaneous linear equations. This will fail if isFullRank() * returns false. * - * @author Paul Meagher + * @author Paul Meagher * - * @version 1.1 + * @version 1.1 */ class QRDecomposition { @@ -54,47 +54,43 @@ class QRDecomposition /** * QR Decomposition computed by Householder reflections. * - * @param matrix $A Rectangular matrix + * @param Matrix $A Rectangular matrix */ - public function __construct($A) + public function __construct(Matrix $A) { - if ($A instanceof Matrix) { - // 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; + // 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]); } - } else { - throw new CalculationException(Matrix::ARGUMENT_TYPE_EXCEPTION); + 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; } } @@ -211,7 +207,7 @@ class QRDecomposition if ($this->isFullRank()) { // Copy right hand side $nx = $B->getColumnDimension(); - $X = $B->getArrayCopy(); + $X = $B->getArray(); // Compute Y = transpose(Q)*B for ($k = 0; $k < $this->n; ++$k) { for ($j = 0; $j < $nx; ++$j) { diff --git a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php index afd9ed0f..6c8999d0 100644 --- a/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php +++ b/src/PhpSpreadsheet/Shared/JAMA/SingularValueDecomposition.php @@ -65,7 +65,7 @@ class SingularValueDecomposition public function __construct($Arg) { // Initialize. - $A = $Arg->getArrayCopy(); + $A = $Arg->getArray(); $this->m = $Arg->getRowDimension(); $this->n = $Arg->getColumnDimension(); $nu = min($this->m, $this->n); diff --git a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php index cee5cd99..1863ae34 100644 --- a/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php +++ b/src/PhpSpreadsheet/Shared/OLE/ChainedBlockStream.php @@ -42,7 +42,7 @@ class ChainedBlockStream * ole-chainedblockstream://oleInstanceId=1 * @param string $mode only "r" is supported * @param int $options mask of STREAM_REPORT_ERRORS and STREAM_USE_PATH - * @param string &$openedPath absolute path of the opened stream (out parameter) + * @param string $openedPath absolute path of the opened stream (out parameter) * * @return bool true on success */ diff --git a/src/PhpSpreadsheet/Shared/OLE/PPS.php b/src/PhpSpreadsheet/Shared/OLE/PPS.php index a90f193b..104b0d6a 100644 --- a/src/PhpSpreadsheet/Shared/OLE/PPS.php +++ b/src/PhpSpreadsheet/Shared/OLE/PPS.php @@ -200,7 +200,7 @@ class PPS * Updates index and pointers to previous, next and children PPS's for this * PPS. I don't think it'll work with Dir PPS's. * - * @param array &$raList Reference to the array of PPS's for the whole OLE + * @param array $raList Reference to the array of PPS's for the whole OLE * container * @param mixed $to_save * @param mixed $depth diff --git a/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php b/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php index 5466d2bc..2fe41055 100644 --- a/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php +++ b/src/PhpSpreadsheet/Shared/OLE/PPS/Root.php @@ -237,7 +237,7 @@ class Root extends PPS * Saving big data (PPS's with data bigger than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). * * @param int $iStBlk - * @param array &$raList Reference to array of PPS's + * @param array $raList Reference to array of PPS's */ private function saveBigData($iStBlk, &$raList): void { @@ -267,7 +267,7 @@ class Root extends PPS /** * get small data (PPS's with data smaller than \PhpOffice\PhpSpreadsheet\Shared\OLE::OLE_DATA_SIZE_SMALL). * - * @param array &$raList Reference to array of PPS's + * @param array $raList Reference to array of PPS's * * @return string */ diff --git a/src/PhpSpreadsheet/Shared/OLERead.php b/src/PhpSpreadsheet/Shared/OLERead.php index 7112b090..78417741 100644 --- a/src/PhpSpreadsheet/Shared/OLERead.php +++ b/src/PhpSpreadsheet/Shared/OLERead.php @@ -92,10 +92,8 @@ class OLERead /** * Read the file. - * - * @param $pFilename string Filename */ - public function read($pFilename): void + public function read(string $pFilename): void { File::assertFile($pFilename); diff --git a/src/PhpSpreadsheet/Shared/StringHelper.php b/src/PhpSpreadsheet/Shared/StringHelper.php index a3cc359b..e85ce55d 100644 --- a/src/PhpSpreadsheet/Shared/StringHelper.php +++ b/src/PhpSpreadsheet/Shared/StringHelper.php @@ -556,7 +556,7 @@ class StringHelper * Identify whether a string contains a fractional numeric value, * and convert it to a numeric if it is. * - * @param string &$operand string value to test + * @param string $operand string value to test * * @return bool */ diff --git a/src/PhpSpreadsheet/Shared/Trend/Trend.php b/src/PhpSpreadsheet/Shared/Trend/Trend.php index 24570d59..61d1183a 100644 --- a/src/PhpSpreadsheet/Shared/Trend/Trend.php +++ b/src/PhpSpreadsheet/Shared/Trend/Trend.php @@ -44,7 +44,7 @@ class Trend /** * Cached results for each method when trying to identify which provides the best fit. * - * @var bestFit[] + * @var BestFit[] */ private static $trendCache = []; diff --git a/src/PhpSpreadsheet/Shared/Xls.php b/src/PhpSpreadsheet/Shared/Xls.php index c9eaf378..b3cdbd7d 100644 --- a/src/PhpSpreadsheet/Shared/Xls.php +++ b/src/PhpSpreadsheet/Shared/Xls.php @@ -209,8 +209,7 @@ class Xls */ public static function oneAnchor2twoAnchor($sheet, $coordinates, $offsetX, $offsetY, $width, $height) { - [$column, $row] = Coordinate::coordinateFromString($coordinates); - $col_start = Coordinate::columnIndexFromString($column); + [$col_start, $row] = Coordinate::indexesFromString($coordinates); $row_start = $row - 1; $x1 = $offsetX; diff --git a/src/PhpSpreadsheet/Style/Border.php b/src/PhpSpreadsheet/Style/Border.php index d11fa0ca..dee1ad4c 100644 --- a/src/PhpSpreadsheet/Style/Border.php +++ b/src/PhpSpreadsheet/Style/Border.php @@ -70,17 +70,19 @@ class Border extends Supervisor */ public function getSharedComponent() { + /** @var Borders $sharedComponent */ + $sharedComponent = $this->parent->getSharedComponent(); switch ($this->parentPropertyName) { case 'bottom': - return $this->parent->getSharedComponent()->getBottom(); + return $sharedComponent->getBottom(); case 'diagonal': - return $this->parent->getSharedComponent()->getDiagonal(); + return $sharedComponent->getDiagonal(); case 'left': - return $this->parent->getSharedComponent()->getLeft(); + return $sharedComponent->getLeft(); case 'right': - return $this->parent->getSharedComponent()->getRight(); + return $sharedComponent->getRight(); case 'top': - return $this->parent->getSharedComponent()->getTop(); + return $sharedComponent->getTop(); } throw new PhpSpreadsheetException('Cannot get shared component for a pseudo-border.'); diff --git a/src/PhpSpreadsheet/Style/Color.php b/src/PhpSpreadsheet/Style/Color.php index acff2e0b..bf5d093f 100644 --- a/src/PhpSpreadsheet/Style/Color.php +++ b/src/PhpSpreadsheet/Style/Color.php @@ -71,14 +71,16 @@ class Color extends Supervisor */ public function getSharedComponent() { + /** @var Border|Fill $sharedComponent */ + $sharedComponent = $this->parent->getSharedComponent(); if ($this->parentPropertyName === 'endColor') { - return $this->parent->getSharedComponent()->getEndColor(); + return $sharedComponent->getEndColor(); } if ($this->parentPropertyName === 'startColor') { - return $this->parent->getSharedComponent()->getStartColor(); + return $sharedComponent->getStartColor(); } - return $this->parent->getSharedComponent()->getColor(); + return $sharedComponent->getColor(); } /** @@ -200,7 +202,7 @@ class Color extends Supervisor * @param bool $hex Flag indicating whether the component should be returned as a hex or a * decimal value * - * @return string The extracted colour component + * @return int|string The extracted colour component */ private static function getColourComponent($RGB, $offset, $hex = true) { @@ -216,7 +218,7 @@ class Color extends Supervisor * @param bool $hex Flag indicating whether the component should be returned as a hex or a * decimal value * - * @return string The red colour component + * @return int|string The red colour component */ public static function getRed($RGB, $hex = true) { @@ -230,7 +232,7 @@ class Color extends Supervisor * @param bool $hex Flag indicating whether the component should be returned as a hex or a * decimal value * - * @return string The green colour component + * @return int|string The green colour component */ public static function getGreen($RGB, $hex = true) { @@ -244,7 +246,7 @@ class Color extends Supervisor * @param bool $hex Flag indicating whether the component should be returned as a hex or a * decimal value * - * @return string The blue colour component + * @return int|string The blue colour component */ public static function getBlue($RGB, $hex = true) { @@ -264,8 +266,11 @@ class Color extends Supervisor $rgba = (strlen($hex) === 8); $adjustPercentage = max(-1.0, min(1.0, $adjustPercentage)); + /** @var int $red */ $red = self::getRed($hex, false); + /** @var int $green */ $green = self::getGreen($hex, false); + /** @var int $blue */ $blue = self::getBlue($hex, false); if ($adjustPercentage > 0) { $red += (255 - $red) * $adjustPercentage; diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php index c6370b86..107969bf 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormatValueObject.php @@ -13,8 +13,6 @@ class ConditionalFormatValueObject /** * ConditionalFormatValueObject constructor. * - * @param $type - * @param $value * @param null|mixed $cellFormula */ public function __construct($type, $value = null, $cellFormula = null) diff --git a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php index 943c734b..899bbe43 100644 --- a/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php +++ b/src/PhpSpreadsheet/Style/ConditionalFormatting/ConditionalFormattingRuleExtension.php @@ -24,8 +24,6 @@ class ConditionalFormattingRuleExtension /** * ConditionalFormattingRuleExtension constructor. - * - * @param $id */ public function __construct($id = null, string $cfRule = self::CONDITION_EXTENSION_DATABAR) { diff --git a/src/PhpSpreadsheet/Style/Style.php b/src/PhpSpreadsheet/Style/Style.php index d3653ed5..224c0feb 100644 --- a/src/PhpSpreadsheet/Style/Style.php +++ b/src/PhpSpreadsheet/Style/Style.php @@ -202,18 +202,17 @@ class Style extends Supervisor // Calculate range outer borders $rangeStart = Coordinate::coordinateFromString($rangeA); $rangeEnd = Coordinate::coordinateFromString($rangeB); + $rangeStartIndexes = Coordinate::indexesFromString($rangeA); + $rangeEndIndexes = Coordinate::indexesFromString($rangeB); - // Translate column into index - $rangeStart0 = $rangeStart[0]; - $rangeEnd0 = $rangeEnd[0]; - $rangeStart[0] = Coordinate::columnIndexFromString($rangeStart[0]); - $rangeEnd[0] = Coordinate::columnIndexFromString($rangeEnd[0]); + $columnStart = $rangeStart[0]; + $columnEnd = $rangeEnd[0]; // Make sure we can loop upwards on rows and columns - if ($rangeStart[0] > $rangeEnd[0] && $rangeStart[1] > $rangeEnd[1]) { - $tmp = $rangeStart; - $rangeStart = $rangeEnd; - $rangeEnd = $tmp; + if ($rangeStartIndexes[0] > $rangeEndIndexes[0] && $rangeStartIndexes[1] > $rangeEndIndexes[1]) { + $tmp = $rangeStartIndexes; + $rangeStartIndexes = $rangeEndIndexes; + $rangeEndIndexes = $tmp; } // ADVANCED MODE: @@ -249,19 +248,19 @@ class Style extends Supervisor unset($pStyles['borders']['inside']); // not needed any more } // width and height characteristics of selection, 1, 2, or 3 (for 3 or more) - $xMax = min($rangeEnd[0] - $rangeStart[0] + 1, 3); - $yMax = min($rangeEnd[1] - $rangeStart[1] + 1, 3); + $xMax = min($rangeEndIndexes[0] - $rangeStartIndexes[0] + 1, 3); + $yMax = min($rangeEndIndexes[1] - $rangeStartIndexes[1] + 1, 3); // loop through up to 3 x 3 = 9 regions for ($x = 1; $x <= $xMax; ++$x) { // start column index for region $colStart = ($x == 3) ? - Coordinate::stringFromColumnIndex($rangeEnd[0]) - : Coordinate::stringFromColumnIndex($rangeStart[0] + $x - 1); + Coordinate::stringFromColumnIndex($rangeEndIndexes[0]) + : Coordinate::stringFromColumnIndex($rangeStartIndexes[0] + $x - 1); // end column index for region $colEnd = ($x == 1) ? - Coordinate::stringFromColumnIndex($rangeStart[0]) - : Coordinate::stringFromColumnIndex($rangeEnd[0] - $xMax + $x); + Coordinate::stringFromColumnIndex($rangeStartIndexes[0]) + : Coordinate::stringFromColumnIndex($rangeEndIndexes[0] - $xMax + $x); for ($y = 1; $y <= $yMax; ++$y) { // which edges are touching the region @@ -285,11 +284,11 @@ class Style extends Supervisor // start row index for region $rowStart = ($y == 3) ? - $rangeEnd[1] : $rangeStart[1] + $y - 1; + $rangeEndIndexes[1] : $rangeStartIndexes[1] + $y - 1; // end row index for region $rowEnd = ($y == 1) ? - $rangeStart[1] : $rangeEnd[1] - $yMax + $y; + $rangeStartIndexes[1] : $rangeEndIndexes[1] - $yMax + $y; // build range for region $range = $colStart . $rowStart . ':' . $colEnd . $rowEnd; @@ -349,7 +348,7 @@ class Style extends Supervisor } // First loop through columns, rows, or cells to find out which styles are affected by this operation - $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStart, $rangeEnd, $rangeStart0, $rangeEnd0, $pStyles); + $oldXfIndexes = $this->getOldXfIndexes($selectionType, $rangeStartIndexes, $rangeEndIndexes, $columnStart, $columnEnd, $pStyles); // clone each of the affected styles, apply the style array, and add the new styles to the workbook $workbook = $this->getActiveSheet()->getParent(); @@ -372,7 +371,7 @@ class Style extends Supervisor // Loop through columns, rows, or cells again and update the XF index switch ($selectionType) { case 'COLUMN': - for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { + for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { $columnDimension = $this->getActiveSheet()->getColumnDimensionByColumn($col); $oldXfIndex = $columnDimension->getXfIndex(); $columnDimension->setXfIndex($newXfIndexes[$oldXfIndex]); @@ -380,7 +379,7 @@ class Style extends Supervisor break; case 'ROW': - for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { $rowDimension = $this->getActiveSheet()->getRowDimension($row); // row without explicit style should be formatted based on default style $oldXfIndex = $rowDimension->getXfIndex() ?? 0; @@ -389,8 +388,8 @@ class Style extends Supervisor break; case 'CELL': - for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { - for ($row = $rangeStart[1]; $row <= $rangeEnd[1]; ++$row) { + for ($col = $rangeStartIndexes[0]; $col <= $rangeEndIndexes[0]; ++$col) { + for ($row = $rangeStartIndexes[1]; $row <= $rangeEndIndexes[1]; ++$row) { $cell = $this->getActiveSheet()->getCellByColumnAndRow($col, $row); $oldXfIndex = $cell->getXfIndex(); $cell->setXfIndex($newXfIndexes[$oldXfIndex]); @@ -427,7 +426,7 @@ class Style extends Supervisor return $this; } - private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $rangeStart0, string $rangeEnd0, array $pStyles): array + private function getOldXfIndexes(string $selectionType, array $rangeStart, array $rangeEnd, string $columnStart, string $columnEnd, array $pStyles): array { $oldXfIndexes = []; switch ($selectionType) { @@ -435,7 +434,7 @@ class Style extends Supervisor for ($col = $rangeStart[0]; $col <= $rangeEnd[0]; ++$col) { $oldXfIndexes[$this->getActiveSheet()->getColumnDimensionByColumn($col)->getXfIndex()] = true; } - foreach ($this->getActiveSheet()->getColumnIterator($rangeStart0, $rangeEnd0) as $columnIterator) { + foreach ($this->getActiveSheet()->getColumnIterator($columnStart, $columnEnd) as $columnIterator) { $cellIterator = $columnIterator->getCellIterator(); $cellIterator->setIterateOnlyExistingCells(true); foreach ($cellIterator as $columnCell) { diff --git a/src/PhpSpreadsheet/Worksheet/AutoFilter.php b/src/PhpSpreadsheet/Worksheet/AutoFilter.php index dc876ee9..d8912b21 100644 --- a/src/PhpSpreadsheet/Worksheet/AutoFilter.php +++ b/src/PhpSpreadsheet/Worksheet/AutoFilter.php @@ -780,7 +780,7 @@ class AutoFilter $ruleValues = []; $dataRowCount = $rangeEnd[1] - $rangeStart[1]; $toptenRuleType = null; - $ruleValue = null; + $ruleValue = 0; $ruleOperator = null; foreach ($rules as $rule) { // We should only ever have one Dynamic Filter Rule anyway diff --git a/src/PhpSpreadsheet/Worksheet/Worksheet.php b/src/PhpSpreadsheet/Worksheet/Worksheet.php index 09ce3e61..f8b6a743 100644 --- a/src/PhpSpreadsheet/Worksheet/Worksheet.php +++ b/src/PhpSpreadsheet/Worksheet/Worksheet.php @@ -96,14 +96,14 @@ class Worksheet implements IComparable /** * Collection of drawings. * - * @var BaseDrawing[] + * @var ArrayObject */ private $drawingCollection; /** * Collection of Chart objects. * - * @var Chart[] + * @var ArrayObject */ private $chartCollection = []; @@ -180,7 +180,7 @@ class Worksheet implements IComparable /** * Collection of breaks. * - * @var array + * @var int[] */ private $breaks = []; @@ -534,7 +534,7 @@ class Worksheet implements IComparable /** * Get collection of drawings. * - * @return BaseDrawing[] + * @return ArrayObject */ public function getDrawingCollection() { @@ -544,7 +544,7 @@ class Worksheet implements IComparable /** * Get collection of charts. * - * @return Chart[] + * @return ArrayObject */ public function getChartCollection() { @@ -1482,7 +1482,7 @@ class Worksheet implements IComparable * Set conditional styles. * * @param string $pCoordinate eg: 'A1' - * @param $pValue Conditional[] + * @param Conditional[] $pValue * * @return $this */ @@ -1640,7 +1640,7 @@ class Worksheet implements IComparable /** * Get breaks. * - * @return array[] + * @return int[] */ public function getBreaks() { diff --git a/src/PhpSpreadsheet/Writer/Html.php b/src/PhpSpreadsheet/Writer/Html.php index 19dfc558..60612737 100644 --- a/src/PhpSpreadsheet/Writer/Html.php +++ b/src/PhpSpreadsheet/Writer/Html.php @@ -453,10 +453,8 @@ class Html extends BaseWriter // Get worksheet dimension [$min, $max] = explode(':', $sheet->calculateWorksheetDataDimension()); - [$minCol, $minRow] = Coordinate::coordinateFromString($min); - $minCol = Coordinate::columnIndexFromString($minCol); - [$maxCol, $maxRow] = Coordinate::coordinateFromString($max); - $maxCol = Coordinate::columnIndexFromString($maxCol); + [$minCol, $minRow] = Coordinate::indexesFromString($min); + [$maxCol, $maxRow] = Coordinate::indexesFromString($max); [$theadStart, $theadEnd, $tbodyStart] = $this->generateSheetStarts($sheet, $minRow); @@ -1703,11 +1701,11 @@ class Html extends BaseWriter $first = $cells[0]; $last = $cells[1]; - [$fc, $fr] = Coordinate::coordinateFromString($first); - $fc = Coordinate::columnIndexFromString($fc) - 1; + [$fc, $fr] = Coordinate::indexesFromString($first); + $fc = $fc - 1; - [$lc, $lr] = Coordinate::coordinateFromString($last); - $lc = Coordinate::columnIndexFromString($lc) - 1; + [$lc, $lr] = Coordinate::indexesFromString($last); + $lc = $lc - 1; // loop through the individual cells in the individual merge $r = $fr - 1; diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index 36f3e9ca..f2d535ac 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -2,7 +2,6 @@ namespace PhpOffice\PhpSpreadsheet\Writer; -use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; use PhpOffice\PhpSpreadsheet\Writer\Ods\Content; @@ -32,6 +31,41 @@ class Ods extends BaseWriter */ private $spreadSheet; + /** + * @var Content + */ + private $writerPartContent; + + /** + * @var Meta + */ + private $writerPartMeta; + + /** + * @var MetaInf + */ + private $writerPartMetaInf; + + /** + * @var Mimetype + */ + private $writerPartMimetype; + + /** + * @var Settings + */ + private $writerPartSettings; + + /** + * @var Styles + */ + private $writerPartStyles; + + /** + * @var Thumbnails + */ + private $writerPartThumbnails; + /** * Create a new Ods. */ @@ -39,35 +73,48 @@ class Ods extends BaseWriter { $this->setSpreadsheet($spreadsheet); - $writerPartsArray = [ - 'content' => Content::class, - 'meta' => Meta::class, - 'meta_inf' => MetaInf::class, - 'mimetype' => Mimetype::class, - 'settings' => Settings::class, - 'styles' => Styles::class, - 'thumbnails' => Thumbnails::class, - ]; - - foreach ($writerPartsArray as $writer => $class) { - $this->writerParts[$writer] = new $class($this); - } + $this->writerPartContent = new Content($this); + $this->writerPartMeta = new Meta($this); + $this->writerPartMetaInf = new MetaInf($this); + $this->writerPartMimetype = new Mimetype($this); + $this->writerPartSettings = new Settings($this); + $this->writerPartStyles = new Styles($this); + $this->writerPartThumbnails = new Thumbnails($this); } - /** - * Get writer part. - * - * @param string $pPartName Writer part name - * - * @return null|Ods\WriterPart - */ - public function getWriterPart($pPartName) + public function getWriterPartContent(): Content { - if ($pPartName != '' && isset($this->writerParts[strtolower($pPartName)])) { - return $this->writerParts[strtolower($pPartName)]; - } + return $this->writerPartContent; + } - return null; + public function getWriterPartMeta(): Meta + { + return $this->writerPartMeta; + } + + public function getWriterPartMetaInf(): MetaInf + { + return $this->writerPartMetaInf; + } + + public function getWriterPartMimetype(): Mimetype + { + return $this->writerPartMimetype; + } + + public function getWriterPartSettings(): Settings + { + return $this->writerPartSettings; + } + + public function getWriterPartStyles(): Styles + { + return $this->writerPartStyles; + } + + public function getWriterPartThumbnails(): Thumbnails + { + return $this->writerPartThumbnails; } /** @@ -88,13 +135,13 @@ class Ods extends BaseWriter $zip = $this->createZip(); - $zip->addFile('META-INF/manifest.xml', $this->getWriterPart('meta_inf')->writeManifest()); - $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPart('thumbnails')->writeThumbnail()); - $zip->addFile('content.xml', $this->getWriterPart('content')->write()); - $zip->addFile('meta.xml', $this->getWriterPart('meta')->write()); - $zip->addFile('mimetype', $this->getWriterPart('mimetype')->write()); - $zip->addFile('settings.xml', $this->getWriterPart('settings')->write()); - $zip->addFile('styles.xml', $this->getWriterPart('styles')->write()); + $zip->addFile('META-INF/manifest.xml', $this->getWriterPartMetaInf()->write()); + $zip->addFile('Thumbnails/thumbnail.png', $this->getWriterPartthumbnails()->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 try { diff --git a/src/PhpSpreadsheet/Writer/Ods/Content.php b/src/PhpSpreadsheet/Writer/Ods/Content.php index 10238ebf..e4bd1793 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Content.php +++ b/src/PhpSpreadsheet/Writer/Ods/Content.php @@ -39,7 +39,7 @@ class Content extends WriterPart * * @return string XML Output */ - public function write() + public function write(): string { $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { diff --git a/src/PhpSpreadsheet/Writer/Ods/Meta.php b/src/PhpSpreadsheet/Writer/Ods/Meta.php index 365221f7..cd3054c0 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Meta.php +++ b/src/PhpSpreadsheet/Writer/Ods/Meta.php @@ -3,22 +3,17 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; -use PhpOffice\PhpSpreadsheet\Spreadsheet; class Meta extends WriterPart { /** * Write meta.xml to XML format. * - * @param Spreadsheet $spreadsheet - * * @return string XML Output */ - public function write(?Spreadsheet $spreadsheet = null) + public function write(): string { - if (!$spreadsheet) { - $spreadsheet = $this->getParentWriter()->getSpreadsheet(); - } + $spreadsheet = $this->getParentWriter()->getSpreadsheet(); $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { diff --git a/src/PhpSpreadsheet/Writer/Ods/MetaInf.php b/src/PhpSpreadsheet/Writer/Ods/MetaInf.php index c9085cf8..f3f0d5fc 100644 --- a/src/PhpSpreadsheet/Writer/Ods/MetaInf.php +++ b/src/PhpSpreadsheet/Writer/Ods/MetaInf.php @@ -11,7 +11,7 @@ class MetaInf extends WriterPart * * @return string XML Output */ - public function writeManifest() + public function write(): string { $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { diff --git a/src/PhpSpreadsheet/Writer/Ods/Mimetype.php b/src/PhpSpreadsheet/Writer/Ods/Mimetype.php index 4aac3685..e109e6e7 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Mimetype.php +++ b/src/PhpSpreadsheet/Writer/Ods/Mimetype.php @@ -2,18 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; -use PhpOffice\PhpSpreadsheet\Spreadsheet; - class Mimetype extends WriterPart { /** * Write mimetype to plain text format. * - * @param Spreadsheet $spreadsheet - * * @return string XML Output */ - public function write(?Spreadsheet $spreadsheet = null) + public function write(): string { return 'application/vnd.oasis.opendocument.spreadsheet'; } diff --git a/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php b/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php index 9edc5c64..ae1c4217 100644 --- a/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php +++ b/src/PhpSpreadsheet/Writer/Ods/NamedExpressions.php @@ -23,11 +23,13 @@ class NamedExpressions $this->formulaConvertor = $formulaConvertor; } - public function write(): void + public function write(): string { $this->objWriter->startElement('table:named-expressions'); $this->writeExpressions(); $this->objWriter->endElement(); + + return ''; } private function writeExpressions(): void diff --git a/src/PhpSpreadsheet/Writer/Ods/Settings.php b/src/PhpSpreadsheet/Writer/Ods/Settings.php index 301daf03..047bd410 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Settings.php +++ b/src/PhpSpreadsheet/Writer/Ods/Settings.php @@ -4,18 +4,15 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; -use PhpOffice\PhpSpreadsheet\Spreadsheet; class Settings extends WriterPart { /** * Write settings.xml to XML format. * - * @param Spreadsheet $spreadsheet - * * @return string XML Output */ - public function write(?Spreadsheet $spreadsheet = null) + public function write(): string { if ($this->getParentWriter()->getUseDiskCaching()) { $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory()); @@ -40,7 +37,7 @@ class Settings extends WriterPart $objWriter->startElement('config:config-item-map-indexed'); $objWriter->writeAttribute('config:name', 'Views'); $objWriter->startElement('config:config-item-map-entry'); - $spreadsheet = $spreadsheet ?? $this->getParentWriter()->getSpreadsheet(); + $spreadsheet = $this->getParentWriter()->getSpreadsheet(); $objWriter->startElement('config:config-item'); $objWriter->writeAttribute('config:name', 'ViewId'); diff --git a/src/PhpSpreadsheet/Writer/Ods/Styles.php b/src/PhpSpreadsheet/Writer/Ods/Styles.php index 7ba7eba7..448b1eff 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Styles.php +++ b/src/PhpSpreadsheet/Writer/Ods/Styles.php @@ -3,18 +3,15 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; use PhpOffice\PhpSpreadsheet\Shared\XMLWriter; -use PhpOffice\PhpSpreadsheet\Spreadsheet; class Styles extends WriterPart { /** * Write styles.xml to XML format. * - * @param Spreadsheet $spreadsheet - * * @return string XML Output */ - public function write(?Spreadsheet $spreadsheet = null) + public function write(): string { $objWriter = null; if ($this->getParentWriter()->getUseDiskCaching()) { diff --git a/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php b/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php index dfab0654..db9579d0 100644 --- a/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php +++ b/src/PhpSpreadsheet/Writer/Ods/Thumbnails.php @@ -2,18 +2,14 @@ namespace PhpOffice\PhpSpreadsheet\Writer\Ods; -use PhpOffice\PhpSpreadsheet\Spreadsheet; - class Thumbnails extends WriterPart { /** * Write Thumbnails/thumbnail.png to PNG format. * - * @param Spreadsheet $spreadsheet - * * @return string XML Output */ - public function writeThumbnail(?Spreadsheet $spreadsheet = null) + public function write(): string { return ''; } diff --git a/src/PhpSpreadsheet/Writer/Ods/WriterPart.php b/src/PhpSpreadsheet/Writer/Ods/WriterPart.php index 1982c450..17d5d169 100644 --- a/src/PhpSpreadsheet/Writer/Ods/WriterPart.php +++ b/src/PhpSpreadsheet/Writer/Ods/WriterPart.php @@ -30,4 +30,6 @@ abstract class WriterPart { $this->parentWriter = $writer; } + + abstract public function write(): string; } diff --git a/src/PhpSpreadsheet/Writer/Xls.php b/src/PhpSpreadsheet/Writer/Xls.php index d458fc74..1ee52bdf 100644 --- a/src/PhpSpreadsheet/Writer/Xls.php +++ b/src/PhpSpreadsheet/Writer/Xls.php @@ -23,6 +23,9 @@ use PhpOffice\PhpSpreadsheet\Spreadsheet; use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; +use PhpOffice\PhpSpreadsheet\Writer\Xls\Parser; +use PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook; +use PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet; class Xls extends BaseWriter { @@ -64,7 +67,7 @@ class Xls extends BaseWriter /** * Formula parser. * - * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Parser + * @var Parser */ private $parser; @@ -90,12 +93,12 @@ class Xls extends BaseWriter private $documentSummaryInformation; /** - * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook + * @var Workbook */ private $writerWorkbook; /** - * @var \PhpOffice\PhpSpreadsheet\Writer\Xls\Worksheet[] + * @var Worksheet[] */ private $writerWorksheets; @@ -388,7 +391,7 @@ class Xls extends BaseWriter } } - private function processMemoryDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing, string $renderingFunctionx): void + private function processMemoryDrawing(BstoreContainer &$bstoreContainer, MemoryDrawing $drawing, string $renderingFunctionx): void { switch ($renderingFunctionx) { case MemoryDrawing::RENDERING_JPEG: @@ -418,7 +421,7 @@ class Xls extends BaseWriter $bstoreContainer->addBSE($BSE); } - private function processDrawing(BstoreContainer &$bstoreContainer, BaseDrawing $drawing): void + private function processDrawing(BstoreContainer &$bstoreContainer, Drawing $drawing): void { $blipType = null; $blipData = ''; diff --git a/src/PhpSpreadsheet/Writer/Xls/Escher.php b/src/PhpSpreadsheet/Writer/Xls/Escher.php index 1ee2e904..e42139b3 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Escher.php +++ b/src/PhpSpreadsheet/Writer/Xls/Escher.php @@ -420,8 +420,8 @@ class Escher $recType = 0xF010; // start coordinates - [$column, $row] = Coordinate::coordinateFromString($this->object->getStartCoordinates()); - $c1 = Coordinate::columnIndexFromString($column) - 1; + [$column, $row] = Coordinate::indexesFromString($this->object->getStartCoordinates()); + $c1 = $column - 1; $r1 = $row - 1; // start offsetX @@ -431,8 +431,8 @@ class Escher $startOffsetY = $this->object->getStartOffsetY(); // end coordinates - [$column, $row] = Coordinate::coordinateFromString($this->object->getEndCoordinates()); - $c2 = Coordinate::columnIndexFromString($column) - 1; + [$column, $row] = Coordinate::indexesFromString($this->object->getEndCoordinates()); + $c2 = $column - 1; $r2 = $row - 1; // end offsetX diff --git a/src/PhpSpreadsheet/Writer/Xls/Parser.php b/src/PhpSpreadsheet/Writer/Xls/Parser.php index 98b2b5cc..d49459b3 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Parser.php +++ b/src/PhpSpreadsheet/Writer/Xls/Parser.php @@ -527,11 +527,11 @@ class Parser } elseif (preg_match('/^' . Calculation::CALCULATION_REGEXP_DEFINEDNAME . '$/mui', $token) && $this->spreadsheet->getDefinedName($token) !== null) { return $this->convertDefinedName($token); // commented so argument number can be processed correctly. See toReversePolish(). - /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) - { - return($this->convertFunction($token, $this->_func_args)); - }*/ - // if it's an argument, ignore the token (the argument remains) + /*elseif (preg_match("/[A-Z0-9\xc0-\xdc\.]+/", $token)) + { + return($this->convertFunction($token, $this->_func_args)); + }*/ + // if it's an argument, ignore the token (the argument remains) } elseif ($token == 'arg') { return ''; } @@ -597,10 +597,9 @@ class Parser if ($args >= 0) { return pack('Cv', $this->ptg['ptgFuncV'], $this->functions[$token][0]); } + // Variable number of args eg. SUM($i, $j, $k, ..). - if ($args == -1) { - return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]); - } + return pack('CCv', $this->ptg['ptgFuncVarV'], $num_args, $this->functions[$token][0]); } /** @@ -852,10 +851,10 @@ class Parser * called by the addWorksheet() method of the * \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook class. * - * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet() - * * @param string $name The name of the worksheet being added * @param int $index The index of the worksheet being added + * + * @see \PhpOffice\PhpSpreadsheet\Writer\Xls\Workbook::addWorksheet() */ public function setExtSheet($name, $index): void { @@ -1231,9 +1230,9 @@ class Parser * This function just introduces a ptgParen element in the tree, so that Excel * doesn't get confused when working with a parenthesized formula afterwards. * - * @see fact() - * * @return array The parsed ptg'd tree + * + * @see fact() */ private function parenthesizedExpression() { @@ -1475,6 +1474,7 @@ class Parser } else { $left_tree = ''; } + // add it's left subtree and return. return $left_tree . $this->convertFunction($tree['value'], $tree['right']); } diff --git a/src/PhpSpreadsheet/Writer/Xls/Workbook.php b/src/PhpSpreadsheet/Writer/Xls/Workbook.php index 831c120b..a917185d 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Workbook.php +++ b/src/PhpSpreadsheet/Writer/Xls/Workbook.php @@ -678,13 +678,13 @@ class Workbook extends BIFFwriter $formulaData = ''; for ($j = 0; $j < $countPrintArea; ++$j) { $printAreaRect = $printArea[$j]; // e.g. A3:J6 - $printAreaRect[0] = Coordinate::coordinateFromString($printAreaRect[0]); - $printAreaRect[1] = Coordinate::coordinateFromString($printAreaRect[1]); + $printAreaRect[0] = Coordinate::indexesFromString($printAreaRect[0]); + $printAreaRect[1] = Coordinate::indexesFromString($printAreaRect[1]); $print_rowmin = $printAreaRect[0][1] - 1; $print_rowmax = $printAreaRect[1][1] - 1; - $print_colmin = Coordinate::columnIndexFromString($printAreaRect[0][0]) - 1; - $print_colmax = Coordinate::columnIndexFromString($printAreaRect[1][0]) - 1; + $print_colmin = $printAreaRect[0][0] - 1; + $print_colmax = $printAreaRect[1][0] - 1; // construct formula data manually because parser does not recognize absolute 3d cell references $formulaData .= pack('Cvvvvv', 0x3B, $i, $print_rowmin, $print_rowmax, $print_colmin, $print_colmax); @@ -756,7 +756,7 @@ class Workbook extends BIFFwriter * Write a short NAME record. * * @param string $name - * @param string $sheetIndex 1-based sheet index the defined name applies to. 0 = global + * @param int $sheetIndex 1-based sheet index the defined name applies to. 0 = global * @param int[][] $rangeBounds range boundaries * @param bool $isHidden * @@ -839,10 +839,9 @@ class Workbook extends BIFFwriter /** * Writes Excel BIFF BOUNDSHEET record. * - * @param Worksheet $sheet Worksheet name * @param int $offset Location of worksheet BOF */ - private function writeBoundSheet($sheet, $offset): void + private function writeBoundSheet(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $sheet, $offset): void { $sheetname = $sheet->getTitle(); $record = 0x0085; // Record identifier diff --git a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php index 8f6015de..fdc05b85 100644 --- a/src/PhpSpreadsheet/Writer/Xls/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xls/Worksheet.php @@ -217,8 +217,8 @@ class Worksheet extends BIFFwriter * * @param int $str_total Total number of strings * @param int $str_unique Total number of unique strings - * @param array &$str_table String Table - * @param array &$colors Colour Table + * @param array $str_table String Table + * @param array $colors Colour Table * @param Parser $parser The formula parser created for the Workbook * @param bool $preCalculateFormulas Flag indicating whether formulas should be calculated or just written * @param \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $phpSheet The worksheet to write @@ -512,7 +512,7 @@ class Worksheet extends BIFFwriter // Hyperlinks foreach ($phpSheet->getHyperLinkCollection() as $coordinate => $hyperlink) { - [$column, $row] = Coordinate::coordinateFromString($coordinate); + [$column, $row] = Coordinate::indexesFromString($coordinate); $url = $hyperlink->getUrl(); @@ -526,7 +526,7 @@ class Worksheet extends BIFFwriter $url = 'external:' . $url; } - $this->writeUrl($row - 1, Coordinate::columnIndexFromString($column) - 1, $url); + $this->writeUrl($row - 1, $column - 1, $url); } $this->writeDataValidity(); @@ -587,10 +587,10 @@ class Worksheet extends BIFFwriter $lastCell = $explodes[1]; } - $firstCellCoordinates = Coordinate::coordinateFromString($firstCell); // e.g. [0, 1] - $lastCellCoordinates = Coordinate::coordinateFromString($lastCell); // e.g. [1, 6] + $firstCellCoordinates = Coordinate::indexesFromString($firstCell); // e.g. [0, 1] + $lastCellCoordinates = Coordinate::indexesFromString($lastCell); // e.g. [1, 6] - return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, Coordinate::columnIndexFromString($firstCellCoordinates[0]) - 1, Coordinate::columnIndexFromString($lastCellCoordinates[0]) - 1); + return pack('vvvv', $firstCellCoordinates[1] - 1, $lastCellCoordinates[1] - 1, $firstCellCoordinates[0] - 1, $lastCellCoordinates[0] - 1); } /** @@ -1455,10 +1455,10 @@ class Worksheet extends BIFFwriter // extract the row and column indexes $range = Coordinate::splitRange($mergeCell); [$first, $last] = $range[0]; - [$firstColumn, $firstRow] = Coordinate::coordinateFromString($first); - [$lastColumn, $lastRow] = Coordinate::coordinateFromString($last); + [$firstColumn, $firstRow] = Coordinate::indexesFromString($first); + [$lastColumn, $lastRow] = Coordinate::indexesFromString($last); - $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, Coordinate::columnIndexFromString($firstColumn) - 1, Coordinate::columnIndexFromString($lastColumn) - 1); + $recordData .= pack('vvvv', $firstRow - 1, $lastRow - 1, $firstColumn - 1, $lastColumn - 1); // flush record if we have reached limit for number of merged cells, or reached final merged cell if ($j == $maxCountMergeCellsPerRecord || $i == $countMergeCells) { @@ -1601,76 +1601,37 @@ class Worksheet extends BIFFwriter */ private function writePanes(): void { - $panes = []; - if ($this->phpSheet->getFreezePane()) { - [$column, $row] = Coordinate::coordinateFromString($this->phpSheet->getFreezePane()); - $panes[0] = Coordinate::columnIndexFromString($column) - 1; - $panes[1] = $row - 1; - - [$leftMostColumn, $topRow] = Coordinate::coordinateFromString($this->phpSheet->getTopLeftCell()); - //Coordinates are zero-based in xls files - $panes[2] = $topRow - 1; - $panes[3] = Coordinate::columnIndexFromString($leftMostColumn) - 1; - } else { + if (!$this->phpSheet->getFreezePane()) { // thaw panes return; } - $x = $panes[0] ?? null; - $y = $panes[1] ?? null; - $rwTop = $panes[2] ?? null; - $colLeft = $panes[3] ?? null; - if (count($panes) > 4) { // if Active pane was received - $pnnAct = $panes[4]; - } else { - $pnnAct = null; - } + [$column, $row] = Coordinate::indexesFromString($this->phpSheet->getFreezePane()); + $x = $column - 1; + $y = $row - 1; + + [$leftMostColumn, $topRow] = Coordinate::indexesFromString($this->phpSheet->getTopLeftCell()); + //Coordinates are zero-based in xls files + $rwTop = $topRow - 1; + $colLeft = $leftMostColumn - 1; + $record = 0x0041; // Record identifier $length = 0x000A; // Number of bytes to follow - // Code specific to frozen or thawed panes. - if ($this->phpSheet->getFreezePane()) { - // Set default values for $rwTop and $colLeft - if (!isset($rwTop)) { - $rwTop = $y; - } - if (!$colLeft) { - $colLeft = $x; - } - } else { - // Set default values for $rwTop and $colLeft - if (!isset($rwTop)) { - $rwTop = 0; - } - if (!$colLeft) { - $colLeft = 0; - } - - // Convert Excel's row and column units to the internal units. - // The default row height is 12.75 - // The default column width is 8.43 - // The following slope and intersection values were interpolated. - // - $y = 20 * $y + 255; - $x = 113.879 * $x + 390; - } - // Determine which pane should be active. There is also the undocumented // option to override this should it be necessary: may be removed later. - // - if (!$pnnAct) { - if ($x != 0 && $y != 0) { - $pnnAct = 0; // Bottom right - } - if ($x != 0 && $y == 0) { - $pnnAct = 1; // Top right - } - if ($x == 0 && $y != 0) { - $pnnAct = 2; // Bottom left - } - if ($x == 0 && $y == 0) { - $pnnAct = 3; // Top left - } + $pnnAct = null; + if ($x != 0 && $y != 0) { + $pnnAct = 0; // Bottom right + } + if ($x != 0 && $y == 0) { + $pnnAct = 1; // Top right + } + if ($x == 0 && $y != 0) { + $pnnAct = 2; // Bottom left + } + if ($x == 0 && $y == 0) { + $pnnAct = 3; // Top left } $this->activePane = $pnnAct; // Used in writeSelection @@ -4427,10 +4388,7 @@ class Worksheet extends BIFFwriter $arrConditional[] = $conditional->getHashCode(); } // Cells - $arrCoord = Coordinate::coordinateFromString($cellCoordinate); - if (!is_numeric($arrCoord[0])) { - $arrCoord[0] = Coordinate::columnIndexFromString($arrCoord[0]); - } + $arrCoord = Coordinate::indexesFromString($cellCoordinate); if ($numColumnMin === null || ($numColumnMin > $arrCoord[0])) { $numColumnMin = $arrCoord[0]; } diff --git a/src/PhpSpreadsheet/Writer/Xlsx.php b/src/PhpSpreadsheet/Writer/Xlsx.php index d71541c8..ea1ce2f2 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx.php +++ b/src/PhpSpreadsheet/Writer/Xlsx.php @@ -5,8 +5,13 @@ namespace PhpOffice\PhpSpreadsheet\Writer; use PhpOffice\PhpSpreadsheet\Calculation\Calculation; use PhpOffice\PhpSpreadsheet\Calculation\Functions; use PhpOffice\PhpSpreadsheet\HashTable; -use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Style\Borders; +use PhpOffice\PhpSpreadsheet\Style\Conditional; +use PhpOffice\PhpSpreadsheet\Style\Fill; +use PhpOffice\PhpSpreadsheet\Style\Font; +use PhpOffice\PhpSpreadsheet\Style\NumberFormat; +use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\Drawing as WorksheetDrawing; use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException; @@ -37,13 +42,6 @@ class Xlsx extends BaseWriter */ private $office2003compatibility = false; - /** - * Private writer parts. - * - * @var Xlsx\WriterPart[] - */ - private $writerParts = []; - /** * Private Spreadsheet. * @@ -61,49 +59,49 @@ class Xlsx extends BaseWriter /** * Private unique Conditional HashTable. * - * @var HashTable + * @var HashTable */ private $stylesConditionalHashTable; /** * Private unique Style HashTable. * - * @var HashTable + * @var HashTable<\PhpOffice\PhpSpreadsheet\Style\Style> */ private $styleHashTable; /** * Private unique Fill HashTable. * - * @var HashTable + * @var HashTable */ private $fillHashTable; /** * Private unique \PhpOffice\PhpSpreadsheet\Style\Font HashTable. * - * @var HashTable + * @var HashTable */ private $fontHashTable; /** * Private unique Borders HashTable. * - * @var HashTable + * @var HashTable */ private $bordersHashTable; /** * Private unique NumberFormat HashTable. * - * @var HashTable + * @var HashTable */ private $numFmtHashTable; /** * Private unique \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable. * - * @var HashTable + * @var HashTable */ private $drawingHashTable; @@ -114,6 +112,71 @@ class Xlsx extends BaseWriter */ private $zip; + /** + * @var Chart + */ + private $writerPartChart; + + /** + * @var Comments + */ + private $writerPartComments; + + /** + * @var ContentTypes + */ + private $writerPartContentTypes; + + /** + * @var DocProps + */ + private $writerPartDocProps; + + /** + * @var Drawing + */ + private $writerPartDrawing; + + /** + * @var Rels + */ + private $writerPartRels; + + /** + * @var RelsRibbon + */ + private $writerPartRelsRibbon; + + /** + * @var RelsVBA + */ + private $writerPartRelsVBA; + + /** + * @var StringTable + */ + private $writerPartStringTable; + + /** + * @var Style + */ + private $writerPartStyle; + + /** + * @var Theme + */ + private $writerPartTheme; + + /** + * @var Workbook + */ + private $writerPartWorkbook; + + /** + * @var Worksheet + */ + private $writerPartWorksheet; + /** * Create a new Xlsx Writer. */ @@ -122,53 +185,93 @@ class Xlsx extends BaseWriter // Assign PhpSpreadsheet $this->setSpreadsheet($spreadsheet); - $writerPartsArray = [ - 'stringtable' => StringTable::class, - 'contenttypes' => ContentTypes::class, - 'docprops' => DocProps::class, - 'rels' => Rels::class, - 'theme' => Theme::class, - 'style' => Style::class, - 'workbook' => Workbook::class, - 'worksheet' => Worksheet::class, - 'drawing' => Drawing::class, - 'comments' => Comments::class, - 'chart' => Chart::class, - 'relsvba' => RelsVBA::class, - 'relsribbonobjects' => RelsRibbon::class, - ]; - - // Initialise writer parts - // and Assign their parent IWriters - foreach ($writerPartsArray as $writer => $class) { - $this->writerParts[$writer] = new $class($this); - } - - $hashTablesArray = ['stylesConditionalHashTable', 'fillHashTable', 'fontHashTable', - 'bordersHashTable', 'numFmtHashTable', 'drawingHashTable', - 'styleHashTable', - ]; + $this->writerPartChart = new Chart($this); + $this->writerPartComments = new Comments($this); + $this->writerPartContentTypes = new ContentTypes($this); + $this->writerPartDocProps = new DocProps($this); + $this->writerPartDrawing = new Drawing($this); + $this->writerPartRels = new Rels($this); + $this->writerPartRelsRibbon = new RelsRibbon($this); + $this->writerPartRelsVBA = new RelsVBA($this); + $this->writerPartStringTable = new StringTable($this); + $this->writerPartStyle = new Style($this); + $this->writerPartTheme = new Theme($this); + $this->writerPartWorkbook = new Workbook($this); + $this->writerPartWorksheet = new Worksheet($this); // Set HashTable variables - foreach ($hashTablesArray as $tableName) { - $this->$tableName = new HashTable(); - } + $this->bordersHashTable = new HashTable(); + $this->drawingHashTable = new HashTable(); + $this->fillHashTable = new HashTable(); + $this->fontHashTable = new HashTable(); + $this->numFmtHashTable = new HashTable(); + $this->styleHashTable = new HashTable(); + $this->stylesConditionalHashTable = new HashTable(); } - /** - * Get writer part. - * - * @param string $pPartName Writer part name - * - * @return \PhpOffice\PhpSpreadsheet\Writer\Xlsx\WriterPart - */ - public function getWriterPart($pPartName) + public function getWriterPartChart(): Chart { - if ($pPartName != '' && isset($this->writerParts[strtolower($pPartName)])) { - return $this->writerParts[strtolower($pPartName)]; - } + return $this->writerPartChart; + } - return null; + public function getWriterPartComments(): Comments + { + return $this->writerPartComments; + } + + public function getWriterPartContentTypes(): ContentTypes + { + return $this->writerPartContentTypes; + } + + public function getWriterPartDocProps(): DocProps + { + return $this->writerPartDocProps; + } + + public function getWriterPartDrawing(): Drawing + { + return $this->writerPartDrawing; + } + + public function getWriterPartRels(): Rels + { + return $this->writerPartRels; + } + + public function getWriterPartRelsRibbon(): RelsRibbon + { + return $this->writerPartRelsRibbon; + } + + public function getWriterPartRelsVBA(): RelsVBA + { + return $this->writerPartRelsVBA; + } + + public function getWriterPartStringTable(): StringTable + { + return $this->writerPartStringTable; + } + + public function getWriterPartStyle(): Style + { + return $this->writerPartStyle; + } + + public function getWriterPartTheme(): Theme + { + return $this->writerPartTheme; + } + + public function getWriterPartWorkbook(): Workbook + { + return $this->writerPartWorkbook; + } + + public function getWriterPartWorksheet(): Worksheet + { + return $this->writerPartWorksheet; } /** @@ -192,19 +295,19 @@ class Xlsx extends BaseWriter // Create string lookup table $this->stringTable = []; for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { - $this->stringTable = $this->getWriterPart('StringTable')->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable); + $this->stringTable = $this->getWriterPartStringTable()->createStringTable($this->spreadSheet->getSheet($i), $this->stringTable); } // Create styles dictionaries - $this->styleHashTable->addFromSource($this->getWriterPart('Style')->allStyles($this->spreadSheet)); - $this->stylesConditionalHashTable->addFromSource($this->getWriterPart('Style')->allConditionalStyles($this->spreadSheet)); - $this->fillHashTable->addFromSource($this->getWriterPart('Style')->allFills($this->spreadSheet)); - $this->fontHashTable->addFromSource($this->getWriterPart('Style')->allFonts($this->spreadSheet)); - $this->bordersHashTable->addFromSource($this->getWriterPart('Style')->allBorders($this->spreadSheet)); - $this->numFmtHashTable->addFromSource($this->getWriterPart('Style')->allNumberFormats($this->spreadSheet)); + $this->styleHashTable->addFromSource($this->getWriterPartStyle()->allStyles($this->spreadSheet)); + $this->stylesConditionalHashTable->addFromSource($this->getWriterPartStyle()->allConditionalStyles($this->spreadSheet)); + $this->fillHashTable->addFromSource($this->getWriterPartStyle()->allFills($this->spreadSheet)); + $this->fontHashTable->addFromSource($this->getWriterPartStyle()->allFonts($this->spreadSheet)); + $this->bordersHashTable->addFromSource($this->getWriterPartStyle()->allBorders($this->spreadSheet)); + $this->numFmtHashTable->addFromSource($this->getWriterPartStyle()->allNumberFormats($this->spreadSheet)); // Create drawing dictionary - $this->drawingHashTable->addFromSource($this->getWriterPart('Drawing')->allDrawings($this->spreadSheet)); + $this->drawingHashTable->addFromSource($this->getWriterPartDrawing()->allDrawings($this->spreadSheet)); $options = new Archive(); $options->setEnableZip64(false); @@ -213,7 +316,7 @@ class Xlsx extends BaseWriter $this->zip = new ZipStream(null, $options); // Add [Content_Types].xml to ZIP file - $this->addZipFile('[Content_Types].xml', $this->getWriterPart('ContentTypes')->writeContentTypes($this->spreadSheet, $this->includeCharts)); + $this->addZipFile('[Content_Types].xml', $this->getWriterPartContentTypes()->writeContentTypes($this->spreadSheet, $this->includeCharts)); //if hasMacros, add the vbaProject.bin file, Certificate file(if exists) if ($this->spreadSheet->hasMacros()) { @@ -225,7 +328,7 @@ class Xlsx extends BaseWriter //signed macros ? // Yes : add the certificate file and the related rels file $this->addZipFile('xl/vbaProjectSignature.bin', $this->spreadSheet->getMacrosCertificate()); - $this->addZipFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPart('RelsVBA')->writeVBARelationships($this->spreadSheet)); + $this->addZipFile('xl/_rels/vbaProject.bin.rels', $this->getWriterPartRelsVBA()->writeVBARelationships($this->spreadSheet)); } } } @@ -240,43 +343,43 @@ class Xlsx extends BaseWriter $this->addZipFile($tmpRootPath . $aPath, $aContent); } //the rels for files - $this->addZipFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPart('RelsRibbonObjects')->writeRibbonRelationships($this->spreadSheet)); + $this->addZipFile($tmpRootPath . '_rels/' . basename($tmpRibbonTarget) . '.rels', $this->getWriterPartRelsRibbon()->writeRibbonRelationships($this->spreadSheet)); } } // Add relationships to ZIP file - $this->addZipFile('_rels/.rels', $this->getWriterPart('Rels')->writeRelationships($this->spreadSheet)); - $this->addZipFile('xl/_rels/workbook.xml.rels', $this->getWriterPart('Rels')->writeWorkbookRelationships($this->spreadSheet)); + $this->addZipFile('_rels/.rels', $this->getWriterPartRels()->writeRelationships($this->spreadSheet)); + $this->addZipFile('xl/_rels/workbook.xml.rels', $this->getWriterPartRels()->writeWorkbookRelationships($this->spreadSheet)); // Add document properties to ZIP file - $this->addZipFile('docProps/app.xml', $this->getWriterPart('DocProps')->writeDocPropsApp($this->spreadSheet)); - $this->addZipFile('docProps/core.xml', $this->getWriterPart('DocProps')->writeDocPropsCore($this->spreadSheet)); - $customPropertiesPart = $this->getWriterPart('DocProps')->writeDocPropsCustom($this->spreadSheet); + $this->addZipFile('docProps/app.xml', $this->getWriterPartDocProps()->writeDocPropsApp($this->spreadSheet)); + $this->addZipFile('docProps/core.xml', $this->getWriterPartDocProps()->writeDocPropsCore($this->spreadSheet)); + $customPropertiesPart = $this->getWriterPartDocProps()->writeDocPropsCustom($this->spreadSheet); if ($customPropertiesPart !== null) { $this->addZipFile('docProps/custom.xml', $customPropertiesPart); } // Add theme to ZIP file - $this->addZipFile('xl/theme/theme1.xml', $this->getWriterPart('Theme')->writeTheme($this->spreadSheet)); + $this->addZipFile('xl/theme/theme1.xml', $this->getWriterPartTheme()->writeTheme($this->spreadSheet)); // Add string table to ZIP file - $this->addZipFile('xl/sharedStrings.xml', $this->getWriterPart('StringTable')->writeStringTable($this->stringTable)); + $this->addZipFile('xl/sharedStrings.xml', $this->getWriterPartStringTable()->writeStringTable($this->stringTable)); // Add styles to ZIP file - $this->addZipFile('xl/styles.xml', $this->getWriterPart('Style')->writeStyles($this->spreadSheet)); + $this->addZipFile('xl/styles.xml', $this->getWriterPartStyle()->writeStyles($this->spreadSheet)); // Add workbook to ZIP file - $this->addZipFile('xl/workbook.xml', $this->getWriterPart('Workbook')->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); + $this->addZipFile('xl/workbook.xml', $this->getWriterPartWorkbook()->writeWorkbook($this->spreadSheet, $this->preCalculateFormulas)); $chartCount = 0; // Add worksheets for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { - $this->addZipFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPart('Worksheet')->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); + $this->addZipFile('xl/worksheets/sheet' . ($i + 1) . '.xml', $this->getWriterPartWorksheet()->writeWorksheet($this->spreadSheet->getSheet($i), $this->stringTable, $this->includeCharts)); if ($this->includeCharts) { $charts = $this->spreadSheet->getSheet($i)->getChartCollection(); if (count($charts) > 0) { foreach ($charts as $chart) { - $this->addZipFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPart('Chart')->writeChart($chart, $this->preCalculateFormulas)); + $this->addZipFile('xl/charts/chart' . ($chartCount + 1) . '.xml', $this->getWriterPartChart()->writeChart($chart, $this->preCalculateFormulas)); ++$chartCount; } } @@ -287,7 +390,7 @@ class Xlsx extends BaseWriter // Add worksheet relationships (drawings, ...) for ($i = 0; $i < $this->spreadSheet->getSheetCount(); ++$i) { // Add relationships - $this->addZipFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); + $this->addZipFile('xl/worksheets/_rels/sheet' . ($i + 1) . '.xml.rels', $this->getWriterPartRels()->writeWorksheetRelationships($this->spreadSheet->getSheet($i), ($i + 1), $this->includeCharts)); // Add unparsedLoadedData $sheetCodeName = $this->spreadSheet->getSheet($i)->getCodeName(); @@ -312,13 +415,13 @@ class Xlsx extends BaseWriter // Add drawing and image relationship parts if (($drawingCount > 0) || ($chartCount > 0)) { // Drawing relationships - $this->addZipFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPart('Rels')->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); + $this->addZipFile('xl/drawings/_rels/drawing' . ($i + 1) . '.xml.rels', $this->getWriterPartRels()->writeDrawingRelationships($this->spreadSheet->getSheet($i), $chartRef1, $this->includeCharts)); // Drawings - $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPartDrawing()->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); } elseif (isset($unparsedLoadedData['sheets'][$sheetCodeName]['drawingAlternateContents'])) { // Drawings - $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPart('Drawing')->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); + $this->addZipFile('xl/drawings/drawing' . ($i + 1) . '.xml', $this->getWriterPartDrawing()->writeDrawings($this->spreadSheet->getSheet($i), $this->includeCharts)); } // Add unparsed drawings @@ -335,10 +438,10 @@ class Xlsx extends BaseWriter // Add comment relationship parts if (count($this->spreadSheet->getSheet($i)->getComments()) > 0) { // VML Comments - $this->addZipFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPart('Comments')->writeVMLComments($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/drawings/vmlDrawing' . ($i + 1) . '.vml', $this->getWriterPartComments()->writeVMLComments($this->spreadSheet->getSheet($i))); // Comments - $this->addZipFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPart('Comments')->writeComments($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/comments' . ($i + 1) . '.xml', $this->getWriterPartComments()->writeComments($this->spreadSheet->getSheet($i))); } // Add unparsed relationship parts @@ -351,10 +454,10 @@ class Xlsx extends BaseWriter // Add header/footer relationship parts if (count($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) { // VML Drawings - $this->addZipFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPart('Drawing')->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/drawings/vmlDrawingHF' . ($i + 1) . '.vml', $this->getWriterPartDrawing()->writeVMLHeaderFooterImages($this->spreadSheet->getSheet($i))); // VML Drawing relationships - $this->addZipFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPart('Rels')->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); + $this->addZipFile('xl/drawings/_rels/vmlDrawingHF' . ($i + 1) . '.vml.rels', $this->getWriterPartRels()->writeHeaderFooterDrawingRelationships($this->spreadSheet->getSheet($i))); // Media foreach ($this->spreadSheet->getSheet($i)->getHeaderFooter()->getImages() as $image) { @@ -445,7 +548,7 @@ class Xlsx extends BaseWriter /** * Get Style HashTable. * - * @return HashTable + * @return HashTable<\PhpOffice\PhpSpreadsheet\Style\Style> */ public function getStyleHashTable() { @@ -455,7 +558,7 @@ class Xlsx extends BaseWriter /** * Get Conditional HashTable. * - * @return HashTable + * @return HashTable */ public function getStylesConditionalHashTable() { @@ -465,7 +568,7 @@ class Xlsx extends BaseWriter /** * Get Fill HashTable. * - * @return HashTable + * @return HashTable */ public function getFillHashTable() { @@ -475,7 +578,7 @@ class Xlsx extends BaseWriter /** * Get \PhpOffice\PhpSpreadsheet\Style\Font HashTable. * - * @return HashTable + * @return HashTable */ public function getFontHashTable() { @@ -485,7 +588,7 @@ class Xlsx extends BaseWriter /** * Get Borders HashTable. * - * @return HashTable + * @return HashTable */ public function getBordersHashTable() { @@ -495,7 +598,7 @@ class Xlsx extends BaseWriter /** * Get NumberFormat HashTable. * - * @return HashTable + * @return HashTable */ public function getNumFmtHashTable() { @@ -505,7 +608,7 @@ class Xlsx extends BaseWriter /** * Get \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet\BaseDrawing HashTable. * - * @return HashTable + * @return HashTable */ public function getDrawingHashTable() { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php index 19da32c4..eefae529 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Chart.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Chart.php @@ -129,7 +129,7 @@ class Chart extends WriterPart if ((is_array($caption)) && (count($caption) > 0)) { $caption = $caption[0]; } - $this->getParentWriter()->getWriterPart('stringtable')->writeRichTextForCharts($objWriter, $caption, 'a'); + $this->getParentWriter()->getWriterPartstringtable()->writeRichTextForCharts($objWriter, $caption, 'a'); $objWriter->endElement(); $objWriter->endElement(); @@ -1040,9 +1040,9 @@ class Chart extends WriterPart * @param DataSeries $plotGroup * @param string $groupType Type of plot for dataseries * @param XMLWriter $objWriter XML Writer - * @param bool &$catIsMultiLevelSeries Is category a multi-series category - * @param bool &$valIsMultiLevelSeries Is value set a multi-series set - * @param string &$plotGroupingType Type of grouping for multi-series values + * @param bool $catIsMultiLevelSeries Is category a multi-series category + * @param bool $valIsMultiLevelSeries Is value set a multi-series set + * @param string $plotGroupingType Type of grouping for multi-series values */ private function writePlotGroup($plotGroup, $groupType, $objWriter, &$catIsMultiLevelSeries, &$valIsMultiLevelSeries, &$plotGroupingType): void { diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php index 73c4308b..51f4248c 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Comments.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Comments.php @@ -79,7 +79,7 @@ class Comments extends WriterPart // text $objWriter->startElement('text'); - $this->getParentWriter()->getWriterPart('stringtable')->writeRichText($objWriter, $pComment->getText()); + $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $pComment->getText()); $objWriter->endElement(); $objWriter->endElement(); @@ -165,8 +165,7 @@ class Comments extends WriterPart private function writeVMLComment(XMLWriter $objWriter, $pCellReference, Comment $pComment): void { // Metadata - [$column, $row] = Coordinate::coordinateFromString($pCellReference); - $column = Coordinate::columnIndexFromString($column); + [$column, $row] = Coordinate::indexesFromString($pCellReference); $id = 1024 + $column + $row; $id = substr($id, 0, 4); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 1713b982..a4b09d39 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -84,22 +84,22 @@ class Drawing extends WriterPart public function writeChart(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Chart\Chart $pChart, $pRelationId = -1): void { $tl = $pChart->getTopLeftPosition(); - $tl['colRow'] = Coordinate::coordinateFromString($tl['cell']); + $tlColRow = Coordinate::indexesFromString($tl['cell']); $br = $pChart->getBottomRightPosition(); - $br['colRow'] = Coordinate::coordinateFromString($br['cell']); + $brColRow = Coordinate::indexesFromString($br['cell']); $objWriter->startElement('xdr:twoCellAnchor'); $objWriter->startElement('xdr:from'); - $objWriter->writeElement('xdr:col', Coordinate::columnIndexFromString($tl['colRow'][0]) - 1); + $objWriter->writeElement('xdr:col', $tlColRow[0] - 1); $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset'])); - $objWriter->writeElement('xdr:row', $tl['colRow'][1] - 1); + $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', Coordinate::columnIndexFromString($br['colRow'][0]) - 1); + $objWriter->writeElement('xdr:col', $brColRow[0] - 1); $objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset'])); - $objWriter->writeElement('xdr:row', $br['colRow'][1] - 1); + $objWriter->writeElement('xdr:row', $brColRow[1] - 1); $objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset'])); $objWriter->endElement(); @@ -158,8 +158,7 @@ class Drawing extends WriterPart // xdr:oneCellAnchor $objWriter->startElement('xdr:oneCellAnchor'); // Image location - $aCoordinates = Coordinate::coordinateFromString($pDrawing->getCoordinates()); - $aCoordinates[0] = Coordinate::columnIndexFromString($aCoordinates[0]); + $aCoordinates = Coordinate::indexesFromString($pDrawing->getCoordinates()); // xdr:from $objWriter->startElement('xdr:from'); @@ -433,7 +432,7 @@ class Drawing extends WriterPart { // Calculate object id preg_match('{(\d+)}', md5($pReference), $m); - $id = 1500 + (substr($m[1], 0, 2) * 1); + $id = 1500 + ((int) substr($m[1], 0, 2) * 1); // Calculate offset $width = $pImage->getWidth(); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index 79841404..a67d3ef8 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -291,7 +291,7 @@ class Rels extends WriterPart /** * Write drawing relationships to XML format. * - * @param int &$chartRef Chart ID + * @param int $chartRef Chart ID * @param bool $includeCharts Flag indicating if we should write charts * * @return string XML Output @@ -425,9 +425,7 @@ class Rels extends WriterPart } /** - * @param $objWriter * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing - * @param $i * * @return int */ diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Theme.php b/src/PhpSpreadsheet/Writer/Xlsx/Theme.php index 3a47be7f..dfde302c 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Theme.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Theme.php @@ -784,13 +784,9 @@ class Theme extends WriterPart /** * Write fonts to XML format. * - * @param XMLWriter $objWriter - * @param string $latinFont - * @param array of string $fontSet - * - * @return string XML Output + * @param string[] $fontSet */ - private function writeFonts($objWriter, $latinFont, $fontSet) + private function writeFonts(XMLWriter $objWriter, string $latinFont, array $fontSet): void { // a:latin $objWriter->startElement('a:latin'); @@ -817,12 +813,8 @@ class Theme extends WriterPart /** * Write colour scheme to XML format. - * - * @param XMLWriter $objWriter - * - * @return string XML Output */ - private function writeColourScheme($objWriter) + private function writeColourScheme(XMLWriter $objWriter): void { foreach (self::$colourScheme as $colourName => $colourValue) { $objWriter->startElement('a:' . $colourName); diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php index 7ad859ac..3978eb6f 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Worksheet.php @@ -1084,7 +1084,7 @@ class Worksheet extends WriterPart private function writeSheetData(XMLWriter $objWriter, PhpspreadsheetWorksheet $pSheet, array $pStringTable): void { // Flipped stringtable, for faster index searching - $aFlippedStringTable = $this->getParentWriter()->getWriterPart('stringtable')->flipStringTable($pStringTable); + $aFlippedStringTable = $this->getParentWriter()->getWriterPartstringtable()->flipStringTable($pStringTable); // sheetData $objWriter->startElement('sheetData'); @@ -1169,7 +1169,7 @@ class Worksheet extends WriterPart $objWriter->writeElement('t', StringHelper::controlCharacterPHP2OOXML(htmlspecialchars($cellValue))); } elseif ($cellValue instanceof RichText) { $objWriter->startElement('is'); - $this->getParentWriter()->getWriterPart('stringtable')->writeRichText($objWriter, $cellValue); + $this->getParentWriter()->getWriterPartstringtable()->writeRichText($objWriter, $cellValue); $objWriter->endElement(); } } diff --git a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php index e71e3ad2..a524a15c 100644 --- a/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/AdvancedValueBinderTest.php @@ -262,9 +262,6 @@ class AdvancedValueBinderTest extends TestCase /** * @dataProvider stringProvider - * - * @param mixed $value - * @param mixed $wrapped */ public function testStringWrapping(string $value, bool $wrapped): void { diff --git a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php index 159af3b9..9225b818 100644 --- a/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php +++ b/tests/PhpSpreadsheetTests/Cell/CoordinateTest.php @@ -96,6 +96,20 @@ class CoordinateTest extends TestCase return require 'tests/data/CellCoordinates.php'; } + /** + * @dataProvider providerIndexesFromString + */ + public function testIndexesFromString(array $expectedResult, string $rangeSet): void + { + $result = Coordinate::indexesFromString($rangeSet); + self::assertSame($expectedResult, $result); + } + + public function providerIndexesFromString(): array + { + return require 'tests/data/Cell/IndexesFromString.php'; + } + public function testCoordinateFromStringWithRangeAddress(): void { $cellAddress = 'A1:AI2012'; diff --git a/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php b/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php index ef22e033..d85de161 100644 --- a/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php +++ b/tests/PhpSpreadsheetTests/Cell/DefaultValueBinderTest.php @@ -8,6 +8,7 @@ use PhpOffice\PhpSpreadsheet\Cell\Cell; use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Cell\DefaultValueBinder; use PhpOffice\PhpSpreadsheet\RichText\RichText; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class DefaultValueBinderTest extends TestCase @@ -15,7 +16,7 @@ class DefaultValueBinderTest extends TestCase private function createCellStub() { // Create a stub for the Cell class. - /** @var Cell $cellStub */ + /** @var Cell&MockObject $cellStub */ $cellStub = $this->getMockBuilder(Cell::class) ->disableOriginalConstructor() ->getMock(); diff --git a/tests/PhpSpreadsheetTests/DefinedNameTest.php b/tests/PhpSpreadsheetTests/DefinedNameTest.php index 8a411775..4d877e6f 100644 --- a/tests/PhpSpreadsheetTests/DefinedNameTest.php +++ b/tests/PhpSpreadsheetTests/DefinedNameTest.php @@ -135,6 +135,7 @@ class DefinedNameTest extends TestCase DefinedName::createInstance('xyz', $this->spreadsheet->getActiveSheet(), 'A1') ); + /** @var NamedRange $namedRange */ $namedRange = $this->spreadsheet->getDefinedName('XYZ'); self::assertInstanceOf(NamedRange::class, $namedRange); self::assertEquals('A1', $namedRange->getRange()); diff --git a/tests/PhpSpreadsheetTests/Functional/ColumnWidthTest.php b/tests/PhpSpreadsheetTests/Functional/ColumnWidthTest.php index 5cd0aec7..045cdcd5 100644 --- a/tests/PhpSpreadsheetTests/Functional/ColumnWidthTest.php +++ b/tests/PhpSpreadsheetTests/Functional/ColumnWidthTest.php @@ -15,8 +15,6 @@ class ColumnWidthTest extends AbstractFunctional /** * @dataProvider providerFormats - * - * @param $format */ public function testReadColumnWidth($format): void { diff --git a/tests/PhpSpreadsheetTests/Functional/CommentsTest.php b/tests/PhpSpreadsheetTests/Functional/CommentsTest.php index 5ba4e7c8..2b08c9a6 100644 --- a/tests/PhpSpreadsheetTests/Functional/CommentsTest.php +++ b/tests/PhpSpreadsheetTests/Functional/CommentsTest.php @@ -21,8 +21,6 @@ class CommentsTest extends AbstractFunctional * count of comments in correct coords. * * @dataProvider providerFormats - * - * @param $format */ public function testComments($format): void { diff --git a/tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php b/tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php index 3a417791..176e3b75 100644 --- a/tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php +++ b/tests/PhpSpreadsheetTests/Reader/CsvContiguousTest.php @@ -23,8 +23,8 @@ class CsvContiguousTest extends TestCase // Tell the Reader that we want to use the Read Filter that we've Instantiated // and that we want to store it in contiguous rows/columns self::assertFalse($reader->getContiguous()); - $reader->setReadFilter($chunkFilter) - ->setContiguous(true); + $reader->setReadFilter($chunkFilter); + $reader->setContiguous(true); // Instantiate a new PhpSpreadsheet object manually $spreadsheet = new Spreadsheet(); @@ -65,8 +65,8 @@ class CsvContiguousTest extends TestCase // Tell the Reader that we want to use the Read Filter that we've Instantiated // and that we want to store it in contiguous rows/columns - $reader->setReadFilter($chunkFilter) - ->setContiguous(true); + $reader->setReadFilter($chunkFilter); + $reader->setContiguous(true); // Instantiate a new PhpSpreadsheet object manually $spreadsheet = new Spreadsheet(); diff --git a/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php b/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php index c32c5743..c434aa60 100644 --- a/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Security/XmlScannerTest.php @@ -23,7 +23,6 @@ class XmlScannerTest extends TestCase * * @param mixed $filename * @param mixed $expectedResult - * @param $libxmlDisableEntityLoader */ public function testValidXML($filename, $expectedResult, $libxmlDisableEntityLoader): void { @@ -59,7 +58,6 @@ class XmlScannerTest extends TestCase * @dataProvider providerInvalidXML * * @param mixed $filename - * @param $libxmlDisableEntityLoader */ public function testInvalidXML($filename, $libxmlDisableEntityLoader): void { diff --git a/tests/PhpSpreadsheetTests/Reader/XlsxTest.php b/tests/PhpSpreadsheetTests/Reader/XlsxTest.php index cb84a3b7..1e240283 100644 --- a/tests/PhpSpreadsheetTests/Reader/XlsxTest.php +++ b/tests/PhpSpreadsheetTests/Reader/XlsxTest.php @@ -250,7 +250,6 @@ class XlsxTest extends TestCase * Test if all whitespace is removed from a style definition string. * This is needed to parse it into properties with the correct keys. * - * @param $string * @dataProvider providerStripsWhiteSpaceFromStyleString */ public function testStripsWhiteSpaceFromStyleString($string): void diff --git a/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php b/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php index 2b66c7b4..d53135f9 100644 --- a/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php +++ b/tests/PhpSpreadsheetTests/Reader/Xml/XmlTest.php @@ -10,8 +10,6 @@ class XmlTest extends TestCase { /** * @dataProvider providerInvalidSimpleXML - * - * @param $filename */ public function testInvalidSimpleXML($filename): void { diff --git a/tests/PhpSpreadsheetTests/SpreadsheetTest.php b/tests/PhpSpreadsheetTests/SpreadsheetTest.php index 129bea7c..cf293001 100644 --- a/tests/PhpSpreadsheetTests/SpreadsheetTest.php +++ b/tests/PhpSpreadsheetTests/SpreadsheetTest.php @@ -44,9 +44,6 @@ class SpreadsheetTest extends TestCase } /** - * @param $index - * @param $sheetName - * * @dataProvider dataProviderForSheetNames */ public function testGetSheetByName($index, $sheetName): void diff --git a/tests/data/Cell/IndexesFromString.php b/tests/data/Cell/IndexesFromString.php new file mode 100644 index 00000000..c2fe1c09 --- /dev/null +++ b/tests/data/Cell/IndexesFromString.php @@ -0,0 +1,74 @@ + Date: Mon, 5 Apr 2021 09:18:55 +0900 Subject: [PATCH 85/89] Drop obsolete code --- src/PhpSpreadsheet/Writer/Ods.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/PhpSpreadsheet/Writer/Ods.php b/src/PhpSpreadsheet/Writer/Ods.php index f2d535ac..f07ade9a 100644 --- a/src/PhpSpreadsheet/Writer/Ods.php +++ b/src/PhpSpreadsheet/Writer/Ods.php @@ -17,13 +17,6 @@ use ZipStream\ZipStream; class Ods extends BaseWriter { - /** - * Private writer parts. - * - * @var Ods\WriterPart[] - */ - private $writerParts = []; - /** * Private PhpSpreadsheet. * From 59de56bb62fde390cf52cef69ffab336a84d0850 Mon Sep 17 00:00:00 2001 From: Vivek Kumar Date: Mon, 5 Apr 2021 12:18:11 +0530 Subject: [PATCH 86/89] Move original file to temporary file --- .../Writer/Xlsx/DrawingsTest.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index 0880bde9..c48c86db 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -5,6 +5,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Writer\Xlsx; use PhpOffice\PhpSpreadsheet\IOFactory; use PhpOffice\PhpSpreadsheet\Reader\Xlsx; use PhpOffice\PhpSpreadsheet\Settings; +use PhpOffice\PhpSpreadsheet\Shared\File; use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional; class DrawingsTest extends AbstractFunctional @@ -50,17 +51,24 @@ class DrawingsTest extends AbstractFunctional public function testSaveLoadWithDrawingWithSamePath(): void { // Read spreadsheet from file - $filePath = 'tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx'; + $originalFilePath = 'tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx'; + + $originalFile = file_get_contents($originalFilePath); + + $tempFilePath = File::sysGetTempDir() . '/saving_drawing_with_same_path'; + + file_put_contents($tempFilePath, $originalFile); + $reader = new Xlsx(); - $spreadsheet = $reader->load($filePath); + $spreadsheet = $reader->load($tempFilePath); $spreadsheet->getActiveSheet()->setCellValue('D5', 'foo'); // Save spreadsheet to file to the same path. Success test case won't // throw exception here $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); - $writer->save($filePath); + $writer->save($tempFilePath); - $reloadedSpreadsheet = $reader->load($filePath); + $reloadedSpreadsheet = $reader->load($tempFilePath); // Fake assert. The only thing we need is to ensure the file is loaded without exception self::assertNotNull($reloadedSpreadsheet); From 95b8c4d59bac92d76a3768fde639967d0fe620be Mon Sep 17 00:00:00 2001 From: oleibman Date: Mon, 5 Apr 2021 07:39:03 -0700 Subject: [PATCH 87/89] Continue MathTrig Breakup - Completion! (#1985) * Continue MathTrig Breakup - Completion! Continuing the process of breaking MathTrip.php up into smaller classes. This round takes care of everything that was left: - ABS - DEGREES - EXP - RADIANS - SQRT - SQRTPI - SUMSQ, SUMX2MY2, SUMX2PY2, SUMXMY2 The only notable logic change was that the 3 SUMX* functions had accepted arrays of unlike length; in that condition, they now return N/A, as Excel does. There had been no tests for this condition. All the functions in MathTrig.php are now deprecated. Except for COMBIN, the test suite executes them only from MathTrig MovedFunctionsTest. COMBIN is still directly called by some Statistics Binomial functions which have not yet had the opportunity to be re-coded for the new location. Co-authored-by: Mark Baker --- .../Calculation/Calculation.php | 24 +-- src/PhpSpreadsheet/Calculation/DateTime.php | 8 +- .../Calculation/DateTimeExcel/Helpers.php | 16 -- src/PhpSpreadsheet/Calculation/MathTrig.php | 140 +++++------------ .../Calculation/MathTrig/Absolute.php | 28 ++++ .../Calculation/MathTrig/Degrees.php | 28 ++++ .../Calculation/MathTrig/Exp.php | 28 ++++ .../Calculation/MathTrig/Radians.php | 28 ++++ .../Calculation/MathTrig/Sqrt.php | 28 ++++ .../Calculation/MathTrig/SqrtPi.php | 29 ++++ .../Calculation/MathTrig/SumSquares.php | 142 ++++++++++++++++++ .../Style/NumberFormat/FractionFormatter.php | 2 +- .../Functions/MathTrig/AbsTest.php | 27 ++-- .../Functions/MathTrig/AllSetupTeardown.php | 15 ++ .../Functions/MathTrig/DegreesTest.php | 27 ++-- .../Functions/MathTrig/ExpTest.php | 31 ++-- .../Functions/MathTrig/MInverseTest.php | 4 +- .../Functions/MathTrig/MMultTest.php | 2 +- .../Functions/MathTrig/MovedFunctionsTest.php | 19 +++ .../Functions/MathTrig/RadiansTest.php | 27 ++-- .../Functions/MathTrig/SqrtPiTest.php | 27 ++-- .../Functions/MathTrig/SqrtTest.php | 25 ++- .../Functions/MathTrig/SumIfsTest.php | 13 +- .../Functions/MathTrig/SumSqTest.php | 25 +-- .../Functions/MathTrig/SumX2MY2Test.php | 30 ++-- .../Functions/MathTrig/SumX2PY2Test.php | 30 ++-- .../Functions/MathTrig/SumXMY2Test.php | 30 ++-- tests/data/Calculation/MathTrig/ABS.php | 16 +- tests/data/Calculation/MathTrig/DEGREES.php | 9 +- tests/data/Calculation/MathTrig/EXP.php | 10 +- tests/data/Calculation/MathTrig/RADIANS.php | 11 +- tests/data/Calculation/MathTrig/SQRT.php | 10 +- tests/data/Calculation/MathTrig/SQRTPI.php | 4 + tests/data/Calculation/MathTrig/SUMSQ.php | 9 ++ tests/data/Calculation/MathTrig/SUMX2MY2.php | 10 ++ tests/data/Calculation/MathTrig/SUMX2PY2.php | 10 ++ tests/data/Calculation/MathTrig/SUMXMY2.php | 10 ++ 37 files changed, 634 insertions(+), 298 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Degrees.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Exp.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Radians.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/SqrtPi.php create mode 100644 src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 1699b5a0..1df103ca 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -228,7 +228,7 @@ class Calculation private static $phpSpreadsheetFunctions = [ 'ABS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinABS'], + 'functionCall' => [MathTrig\Absolute::class, 'evaluate'], 'argumentCount' => '1', ], 'ACCRINT' => [ @@ -835,7 +835,7 @@ class Calculation ], 'DEGREES' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinDEGREES'], + 'functionCall' => [MathTrig\Degrees::class, 'evaluate'], 'argumentCount' => '1', ], 'DELTA' => [ @@ -975,7 +975,7 @@ class Calculation ], 'EXP' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinEXP'], + 'functionCall' => [MathTrig\Exp::class, 'evaluate'], 'argumentCount' => '1', ], 'EXPONDIST' => [ @@ -2038,7 +2038,7 @@ class Calculation ], 'RADIANS' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinRADIANS'], + 'functionCall' => [MathTrig\Radians::class, 'evaluate'], 'argumentCount' => '1', ], 'RAND' => [ @@ -2185,7 +2185,7 @@ class Calculation ], 'SERIESSUM' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SERIESSUM'], + 'functionCall' => [MathTrig\SeriesSum::class, 'funcSeriesSum'], 'argumentCount' => '4', ], 'SHEET' => [ @@ -2205,7 +2205,7 @@ class Calculation ], 'SIN' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinSIN'], + 'functionCall' => [MathTrig\Sin::class, 'funcSin'], 'argumentCount' => '1', ], 'SINH' => [ @@ -2250,12 +2250,12 @@ class Calculation ], 'SQRT' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'builtinSQRT'], + 'functionCall' => [MathTrig\Sqrt::class, 'evaluate'], 'argumentCount' => '1', ], 'SQRTPI' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SQRTPI'], + 'functionCall' => [MathTrig\SqrtPi::class, 'evaluate'], 'argumentCount' => '1', ], 'STANDARDIZE' => [ @@ -2331,22 +2331,22 @@ class Calculation ], 'SUMSQ' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUMSQ'], + 'functionCall' => [MathTrig\SumSquares::class, 'sumSquare'], 'argumentCount' => '1+', ], 'SUMX2MY2' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUMX2MY2'], + 'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredMinusYSquared'], 'argumentCount' => '2', ], 'SUMX2PY2' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUMX2PY2'], + 'functionCall' => [MathTrig\SumSquares::class, 'sumXSquaredPlusYSquared'], 'argumentCount' => '2', ], 'SUMXMY2' => [ 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [MathTrig::class, 'SUMXMY2'], + 'functionCall' => [MathTrig\SumSquares::class, 'sumXMinusYSquared'], 'argumentCount' => '2', ], 'SWITCH' => [ diff --git a/src/PhpSpreadsheet/Calculation/DateTime.php b/src/PhpSpreadsheet/Calculation/DateTime.php index 3b79a6d6..7643ed0b 100644 --- a/src/PhpSpreadsheet/Calculation/DateTime.php +++ b/src/PhpSpreadsheet/Calculation/DateTime.php @@ -23,7 +23,7 @@ class DateTime /** * getDateValue. * - * @Deprecated 2.0.0 Use the method getDateValueNoThrow in the DateTimeExcel\Helpers class instead + * @Deprecated 2.0.0 Use the method getDateValue in the DateTimeExcel\Helpers class instead * * @param mixed $dateValue * @@ -31,7 +31,11 @@ class DateTime */ public static function getDateValue($dateValue) { - return DateTimeExcel\Helpers::getDateValueNoThrow($dateValue); + try { + return DateTimeExcel\Helpers::getDateValue($dateValue); + } catch (Exception $e) { + return $e->getMessage(); + } } /** diff --git a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php index 48300642..636f0c87 100644 --- a/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php +++ b/src/PhpSpreadsheet/Calculation/DateTimeExcel/Helpers.php @@ -56,22 +56,6 @@ class Helpers return (float) $dateValue; } - /** - * getDateValueNoThrow. - * - * @param mixed $dateValue - * - * @return mixed Excel date/time serial value, or string if error - */ - public static function getDateValueNoThrow($dateValue) - { - try { - return self::getDateValue($dateValue); - } catch (Exception $e) { - return $e->getMessage(); - } - } - /** * getTimeValue. * diff --git a/src/PhpSpreadsheet/Calculation/MathTrig.php b/src/PhpSpreadsheet/Calculation/MathTrig.php index e960d7ef..7f30edeb 100644 --- a/src/PhpSpreadsheet/Calculation/MathTrig.php +++ b/src/PhpSpreadsheet/Calculation/MathTrig.php @@ -640,23 +640,15 @@ class MathTrig * * Returns the square root of (number * pi). * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\SqrtPi class instead + * * @param float $number Number * * @return float|string Square Root of Number * Pi, or a string containing an error */ public static function SQRTPI($number) { - $number = Functions::flattenSingleValue($number); - - if (is_numeric($number)) { - if ($number < 0) { - return Functions::NAN(); - } - - return sqrt($number * M_PI); - } - - return Functions::VALUE(); + return MathTrig\SqrtPi::evaluate($number); } /** @@ -769,107 +761,63 @@ class MathTrig * * SUMSQ returns the sum of the squares of the arguments * + * @Deprecated 2.0.0 Use the sumSquare method in the MathTrig\SumSquares class instead + * * Excel Function: * SUMSQ(value1[,value2[, ...]]) * * @param mixed ...$args Data values * - * @return float + * @return float|string */ public static function SUMSQ(...$args) { - $returnValue = 0; - - // Loop through arguments - foreach (Functions::flattenArray($args) as $arg) { - // Is it a numeric value? - if ((is_numeric($arg)) && (!is_string($arg))) { - $returnValue += ($arg * $arg); - } - } - - return $returnValue; + return MathTrig\SumSquares::sumSquare(...$args); } /** * SUMX2MY2. * + * @Deprecated 2.0.0 Use the sumXSquaredMinusYSquared method in the MathTrig\SumSquares class instead + * * @param mixed[] $matrixData1 Matrix #1 * @param mixed[] $matrixData2 Matrix #2 * - * @return float + * @return float|string */ public static function SUMX2MY2($matrixData1, $matrixData2) { - $array1 = Functions::flattenArray($matrixData1); - $array2 = Functions::flattenArray($matrixData2); - $count = min(count($array1), count($array2)); - - $result = 0; - for ($i = 0; $i < $count; ++$i) { - if ( - ((is_numeric($array1[$i])) && (!is_string($array1[$i]))) && - ((is_numeric($array2[$i])) && (!is_string($array2[$i]))) - ) { - $result += ($array1[$i] * $array1[$i]) - ($array2[$i] * $array2[$i]); - } - } - - return $result; + return MathTrig\SumSquares::sumXSquaredMinusYSquared($matrixData1, $matrixData2); } /** * SUMX2PY2. * + * @Deprecated 2.0.0 Use the sumXSquaredPlusYSquared method in the MathTrig\SumSquares class instead + * * @param mixed[] $matrixData1 Matrix #1 * @param mixed[] $matrixData2 Matrix #2 * - * @return float + * @return float|string */ public static function SUMX2PY2($matrixData1, $matrixData2) { - $array1 = Functions::flattenArray($matrixData1); - $array2 = Functions::flattenArray($matrixData2); - $count = min(count($array1), count($array2)); - - $result = 0; - for ($i = 0; $i < $count; ++$i) { - if ( - ((is_numeric($array1[$i])) && (!is_string($array1[$i]))) && - ((is_numeric($array2[$i])) && (!is_string($array2[$i]))) - ) { - $result += ($array1[$i] * $array1[$i]) + ($array2[$i] * $array2[$i]); - } - } - - return $result; + return MathTrig\SumSquares::sumXSquaredPlusYSquared($matrixData1, $matrixData2); } /** * SUMXMY2. * + * @Deprecated 2.0.0 Use the sumXMinusYSquared method in the MathTrig\SumSquares class instead + * * @param mixed[] $matrixData1 Matrix #1 * @param mixed[] $matrixData2 Matrix #2 * - * @return float + * @return float|string */ public static function SUMXMY2($matrixData1, $matrixData2) { - $array1 = Functions::flattenArray($matrixData1); - $array2 = Functions::flattenArray($matrixData2); - $count = min(count($array1), count($array2)); - - $result = 0; - for ($i = 0; $i < $count; ++$i) { - if ( - ((is_numeric($array1[$i])) && (!is_string($array1[$i]))) && - ((is_numeric($array2[$i])) && (!is_string($array2[$i]))) - ) { - $result += ($array1[$i] - $array2[$i]) * ($array1[$i] - $array2[$i]); - } - } - - return $result; + return MathTrig\SumSquares::sumXMinusYSquared($matrixData1, $matrixData2); } /** @@ -1057,19 +1005,15 @@ class MathTrig * * Returns the result of builtin function abs after validating args. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Absolute class instead + * * @param mixed $number Should be numeric * * @return float|int|string Rounded number */ public static function builtinABS($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return abs($number); + return MathTrig\Absolute::evaluate($number); } /** @@ -1205,19 +1149,15 @@ class MathTrig * * Returns the result of builtin function rad2deg after validating args. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Degrees class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinDEGREES($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return rad2deg($number); + return MathTrig\Degrees::evaluate($number); } /** @@ -1225,19 +1165,15 @@ class MathTrig * * Returns the result of builtin function exp after validating args. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Exp class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinEXP($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return exp($number); + return MathTrig\Exp::evaluate($number); } /** @@ -1277,19 +1213,15 @@ class MathTrig * * Returns the result of builtin function deg2rad after validating args. * + * @Deprecated 2.0.0 Use the funcSin method in the MathTrig\Sin class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinRADIANS($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return deg2rad($number); + return MathTrig\Radians::evaluate($number); } /** @@ -1329,19 +1261,15 @@ class MathTrig * * Returns the result of builtin function sqrt after validating args. * + * @Deprecated 2.0.0 Use the evaluate method in the MathTrig\Sqrt class instead + * * @param mixed $number Should be numeric * * @return float|string Rounded number */ public static function builtinSQRT($number) { - $number = Functions::flattenSingleValue($number); - - if (!is_numeric($number)) { - return Functions::VALUE(); - } - - return self::numberOrNan(sqrt($number)); + return MathTrig\Sqrt::evaluate($number); } /** diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php b/src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php new file mode 100644 index 00000000..c2dc579f --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return abs($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Degrees.php b/src/PhpSpreadsheet/Calculation/MathTrig/Degrees.php new file mode 100644 index 00000000..501817be --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Degrees.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return rad2deg($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php b/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php new file mode 100644 index 00000000..f3f8af59 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Exp.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return exp($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Radians.php b/src/PhpSpreadsheet/Calculation/MathTrig/Radians.php new file mode 100644 index 00000000..15e97011 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Radians.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return deg2rad($number); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php b/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php new file mode 100644 index 00000000..aeb38234 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/Sqrt.php @@ -0,0 +1,28 @@ +getMessage(); + } + + return Helpers::numberOrNan(sqrt($number)); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/SqrtPi.php b/src/PhpSpreadsheet/Calculation/MathTrig/SqrtPi.php new file mode 100644 index 00000000..6ff79203 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/SqrtPi.php @@ -0,0 +1,29 @@ +getMessage(); + } + + return sqrt($number * M_PI); + } +} diff --git a/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php b/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php new file mode 100644 index 00000000..a750b149 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/MathTrig/SumSquares.php @@ -0,0 +1,142 @@ +getMessage(); + } + + return $returnValue; + } + + private static function getCount(array $array1, array $array2): int + { + $count = count($array1); + if ($count !== count($array2)) { + throw new Exception(Functions::NA()); + } + + return $count; + } + + /** + * These functions accept only numeric arguments, not even strings which are numeric. + * + * @param mixed $item + */ + private static function numericNotString($item): bool + { + return is_numeric($item) && !is_string($item); + } + + /** + * SUMX2MY2. + * + * @param mixed[] $matrixData1 Matrix #1 + * @param mixed[] $matrixData2 Matrix #2 + * + * @return float|string + */ + public static function sumXSquaredMinusYSquared($matrixData1, $matrixData2) + { + try { + $array1 = Functions::flattenArray($matrixData1); + $array2 = Functions::flattenArray($matrixData2); + $count = self::getCount($array1, $array2); + + $result = 0; + for ($i = 0; $i < $count; ++$i) { + if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { + $result += ($array1[$i] * $array1[$i]) - ($array2[$i] * $array2[$i]); + } + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $result; + } + + /** + * SUMX2PY2. + * + * @param mixed[] $matrixData1 Matrix #1 + * @param mixed[] $matrixData2 Matrix #2 + * + * @return float|string + */ + public static function sumXSquaredPlusYSquared($matrixData1, $matrixData2) + { + try { + $array1 = Functions::flattenArray($matrixData1); + $array2 = Functions::flattenArray($matrixData2); + $count = self::getCount($array1, $array2); + + $result = 0; + for ($i = 0; $i < $count; ++$i) { + if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { + $result += ($array1[$i] * $array1[$i]) + ($array2[$i] * $array2[$i]); + } + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $result; + } + + /** + * SUMXMY2. + * + * @param mixed[] $matrixData1 Matrix #1 + * @param mixed[] $matrixData2 Matrix #2 + * + * @return float|string + */ + public static function sumXMinusYSquared($matrixData1, $matrixData2) + { + try { + $array1 = Functions::flattenArray($matrixData1); + $array2 = Functions::flattenArray($matrixData2); + $count = self::getCount($array1, $array2); + + $result = 0; + for ($i = 0; $i < $count; ++$i) { + if (self::numericNotString($array1[$i]) && self::numericNotString($array2[$i])) { + $result += ($array1[$i] - $array2[$i]) * ($array1[$i] - $array2[$i]); + } + } + } catch (Exception $e) { + return $e->getMessage(); + } + + return $result; + } +} diff --git a/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php index 2b1c7911..48d927f2 100644 --- a/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php +++ b/src/PhpSpreadsheet/Style/NumberFormat/FractionFormatter.php @@ -17,7 +17,7 @@ class FractionFormatter extends BaseFormatter $decimalLength = strlen($decimalPart); $decimalDivisor = 10 ** $decimalLength; - $GCD = MathTrig::GCD($decimalPart, $decimalDivisor); + $GCD = MathTrig\Gcd::evaluate($decimalPart, $decimalDivisor); $adjustedDecimalPart = $decimalPart / $GCD; $adjustedDecimalDivisor = $decimalDivisor / $GCD; diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php index 49816024..7e74474a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AbsTest.php @@ -2,31 +2,26 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class AbsTest extends TestCase +class AbsTest extends AllSetupTeardown { /** * @dataProvider providerAbs * * @param mixed $expectedResult - * @param mixed $val + * @param mixed $number */ - public function testRound($expectedResult, $val = null): void + public function testRound($expectedResult, $number = 'omitted'): void { - if ($val === null) { - $this->expectException(CalcExp::class); - $formula = '=ABS()'; + $sheet = $this->sheet; + $this->mightHaveException($expectedResult); + $this->setCell('A1', $number); + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=ABS()'); } else { - $formula = "=ABS($val)"; + $sheet->getCell('B1')->setValue('=ABS(A1)'); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); - $result = $sheet->getCell('A1')->getCalculatedValue(); - self::assertEqualsWithDelta($expectedResult, $result, 1E-12); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertSame($expectedResult, $result); } public function providerAbs() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php index 86c30c22..eef757f4 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/AllSetupTeardown.php @@ -4,6 +4,7 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException; use PhpOffice\PhpSpreadsheet\Calculation\Functions; +use PhpOffice\PhpSpreadsheet\Cell\DataType; use PhpOffice\PhpSpreadsheet\Spreadsheet; use PHPUnit\Framework\TestCase; @@ -49,4 +50,18 @@ class AllSetupTeardown extends TestCase $this->expectException(CalcException::class); } } + + /** + * @param mixed $value + */ + protected function setCell(string $cell, $value): void + { + if ($value !== null) { + if (is_string($value) && is_numeric($value)) { + $this->sheet->getCell($cell)->setValueExplicit($value, DataType::TYPE_STRING); + } else { + $this->sheet->getCell($cell)->setValue($value); + } + } + } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php index 3f92703b..d441a943 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/DegreesTest.php @@ -2,31 +2,26 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class DegreesTest extends TestCase +class DegreesTest extends AllSetupTeardown { /** * @dataProvider providerDEGREES * * @param mixed $expectedResult - * @param mixed $val + * @param mixed $number */ - public function testDEGREES($expectedResult, $val = null): void + public function testDegrees($expectedResult, $number = 'omitted'): void { - if ($val === null) { - $this->expectException(CalcExp::class); - $formula = '=DEGREES()'; + $sheet = $this->sheet; + $this->mightHaveException($expectedResult); + $this->setCell('A1', $number); + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=DEGREES()'); } else { - $formula = "=DEGREES($val)"; + $sheet->getCell('B1')->setValue('=DEGREES(A1)'); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); - $result = $sheet->getCell('A1')->getCalculatedValue(); - self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } public function providerDegrees() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php index 89bc0097..7e4510fa 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/ExpTest.php @@ -2,31 +2,28 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class ExpTest extends TestCase +class ExpTest extends AllSetupTeardown { /** * @dataProvider providerEXP * * @param mixed $expectedResult - * @param mixed $val + * @param mixed $number */ - public function testEXP($expectedResult, $val = null): void + public function testEXP($expectedResult, $number = 'omitted'): void { - if ($val === null) { - $this->expectException(CalcExp::class); - $formula = '=EXP()'; - } else { - $formula = "=EXP($val)"; + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($number !== null) { + $sheet->getCell('A1')->setValue($number); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); - $result = $sheet->getCell('A1')->getCalculatedValue(); - self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=EXP()'); + } else { + $sheet->getCell('B1')->setValue('=EXP(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } public function providerEXP() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php index 8831fe83..1912fe07 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MInverseTest.php @@ -11,9 +11,9 @@ class MInverseTest extends AllSetupTeardown * * @param mixed $expectedResult */ - public function testMINVERSE($expectedResult, ...$args): void + public function testMINVERSE($expectedResult, array $args): void { - $result = MathTrig::MINVERSE(...$args); + $result = MathTrig\MatrixFunctions::funcMInverse($args); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php index 6c40103c..5e0d45f5 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MMultTest.php @@ -13,7 +13,7 @@ class MMultTest extends AllSetupTeardown */ public function testMMULT($expectedResult, ...$args): void { - $result = MathTrig::MMULT(...$args); + $result = MathTrig\MatrixFunctions::funcMMult(...$args); self::assertEqualsWithDelta($expectedResult, $result, 1E-8); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php index 580092cd..2cca8f36 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/MovedFunctionsTest.php @@ -16,6 +16,7 @@ class MovedFunctionsTest extends TestCase { public function testMovedFunctions(): void { + self::assertSame(1, MathTrig::builtinABS(1)); self::assertEqualsWithDelta(0, MathTrig::builtinACOS(1), 1E-9); self::assertEqualsWithDelta(0, MathTrig::builtinACOSH(1), 1E-9); self::assertEqualsWithDelta(3.04192400109863, MathTrig::ACOT(-10), 1E-9); @@ -35,12 +36,15 @@ class MovedFunctionsTest extends TestCase self::assertEquals('#DIV/0!', MathTrig::COTH(0)); self::assertEquals('#DIV/0!', MathTrig::CSC(0)); self::assertEquals('#DIV/0!', MathTrig::CSCH(0)); + self::assertEquals(0, MathTrig::builtinDEGREES(0)); self::assertEquals(6, MathTrig::EVEN(4.5)); + self::assertEquals(1, MathTrig::builtinEXP(0)); self::assertEquals(6, MathTrig::FACT(3)); self::assertEquals(105, MathTrig::FACTDOUBLE(7)); self::assertEquals(-6, MathTrig::FLOOR(-4.5, 2)); self::assertEquals(0.23, MathTrig::FLOORMATH(0.234, 0.01)); self::assertEquals(-4, MathTrig::FLOORPRECISE(-2.5, 2)); + self::assertEquals(2, MathTrig::GCD(4, 6)); self::assertEquals(-9, MathTrig::INT(-8.3)); self::assertEquals(12, MathTrig::LCM(4, 6)); self::assertEqualswithDelta(2.302585, MathTrig::builtinLN(10), 1E-6); @@ -58,10 +62,12 @@ class MovedFunctionsTest extends TestCase self::assertEquals(1, MathTrig::MOD(5, 2)); self::assertEquals(6, MathTrig::MROUND(7.3, 3)); self::assertEquals(1, MathTrig::MULTINOMIAL(1)); + self::assertEquals(0, MathTrig::numberOrNan(0)); self::assertEquals(5, MathTrig::ODD(4.5)); self::assertEquals(8, MathTrig::POWER(2, 3)); self::assertEquals(8, MathTrig::PRODUCT(1, 2, 4)); self::assertEquals(8, MathTrig::QUOTIENT(17, 2)); + self::assertEquals(0, MathTrig::builtinRADIANS(0)); self::assertGreaterThanOrEqual(0, MATHTRIG::RAND()); self::assertEquals('I', MathTrig::ROMAN(1)); self::assertEquals(3.3, MathTrig::builtinROUND(3.27, 1)); @@ -73,10 +79,23 @@ class MovedFunctionsTest extends TestCase self::assertEquals(1, MathTrig::SIGN(79.2)); self::assertEquals(0, MathTrig::builtinSIN(0)); self::assertEquals(0, MathTrig::builtinSINH(0)); + self::assertEquals(0, MathTrig::builtinSQRT(0)); + self::assertEqualswithDelta(3.54490770181103, MathTrig::SQRTPI(4), 1E-6); self::assertEquals(0, MathTrig::SUBTOTAL(2, [0, 0])); self::assertEquals(7, MathTrig::SUM(1, 2, 4)); self::assertEquals(4, MathTrig::SUMIF([[2], [4]], '>2')); + self::assertEquals(2, MathTrig::SUMIFS( + [[1], [1], [1]], + [['Y'], ['Y'], ['N']], + '=Y', + [['H'], ['H'], ['H']], + '=H' + )); self::assertEquals(17, MathTrig::SUMPRODUCT([1, 2, 3], [5, 0, 4])); + self::assertEquals(21, MathTrig::SUMSQ(1, 2, 4)); + self::assertEquals(-20, MathTrig::SUMX2MY2([1, 2], [3, 4])); + self::assertEquals(30, MathTrig::SUMX2PY2([1, 2], [3, 4])); + self::assertEquals(8, MathTrig::SUMXMY2([1, 2], [3, 4])); self::assertEquals(0, MathTrig::builtinTAN(0)); self::assertEquals(0, MathTrig::builtinTANH(0)); self::assertEquals(70, MathTrig::TRUNC(79.2, -1)); diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php index b5849540..00af620e 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/RadiansTest.php @@ -2,31 +2,26 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class RadiansTest extends TestCase +class RadiansTest extends AllSetupTeardown { /** * @dataProvider providerRADIANS * * @param mixed $expectedResult - * @param mixed $val + * @param mixed $number */ - public function testRADIANS($expectedResult, $val = null): void + public function testRADIANS($expectedResult, $number = 'omitted'): void { - if ($val === null) { - $this->expectException(CalcExp::class); - $formula = '=RADIANS()'; + $sheet = $this->sheet; + $this->mightHaveException($expectedResult); + $this->setCell('A1', $number); + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=RADIANS()'); } else { - $formula = "=RADIANS($val)"; + $sheet->getCell('B1')->setValue('=RADIANS(A1)'); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); - $result = $sheet->getCell('A1')->getCalculatedValue(); - self::assertEqualsWithDelta($expectedResult, $result, 1E-6); + $result = $sheet->getCell('B1')->getCalculatedValue(); + self::assertEqualsWithDelta($expectedResult, $result, 1E-9); } public function providerRADIANS() diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php index c49934ea..fe130d8c 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtPiTest.php @@ -2,26 +2,27 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class SqrtPiTest extends TestCase +class SqrtPiTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSQRTPI * * @param mixed $expectedResult - * @param mixed $value + * @param mixed $number */ - public function testSQRTPI($expectedResult, $value): void + public function testSQRTPI($expectedResult, $number): void { - $result = MathTrig::SQRTPI($value); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + if ($number !== null) { + $sheet->getCell('A1')->setValue($number); + } + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=SQRTPI()'); + } else { + $sheet->getCell('B1')->setValue('=SQRTPI(A1)'); + } + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php index 972035e7..9e82fe70 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SqrtTest.php @@ -2,30 +2,25 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcExp; -use PhpOffice\PhpSpreadsheet\Spreadsheet; -use PHPUnit\Framework\TestCase; - -class SqrtTest extends TestCase +class SqrtTest extends AllSetupTeardown { /** * @dataProvider providerSQRT * * @param mixed $expectedResult - * @param mixed $val + * @param mixed $number */ - public function testSQRT($expectedResult, $val = null): void + public function testSQRT($expectedResult, $number = 'omitted'): void { - if ($val === null) { - $this->expectException(CalcExp::class); - $formula = '=SQRT()'; + $sheet = $this->sheet; + $this->mightHaveException($expectedResult); + $this->setCell('A1', $number); + if ($number === 'omitted') { + $sheet->getCell('B1')->setValue('=SQRT()'); } else { - $formula = "=SQRT($val)"; + $sheet->getCell('B1')->setValue('=SQRT(A1)'); } - $spreadsheet = new Spreadsheet(); - $sheet = $spreadsheet->getActiveSheet(); - $sheet->getCell('A1')->setValue($formula); - $result = $sheet->getCell('A1')->getCalculatedValue(); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-6); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfsTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfsTest.php index b7be17c9..a4a99888 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfsTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumIfsTest.php @@ -2,17 +2,10 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; +use PhpOffice\PhpSpreadsheet\Calculation\Statistical; -class SumIfsTest extends TestCase +class SumIfsTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUMIFS * @@ -20,7 +13,7 @@ class SumIfsTest extends TestCase */ public function testSUMIFS($expectedResult, ...$args): void { - $result = MathTrig::SUMIFS(...$args); + $result = Statistical\Conditional::SUMIFS(...$args); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumSqTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumSqTest.php index f1165e7b..e811dd75 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumSqTest.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumSqTest.php @@ -2,17 +2,8 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; -use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; - -class SumSqTest extends TestCase +class SumSqTest extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUMSQ * @@ -20,7 +11,19 @@ class SumSqTest extends TestCase */ public function testSUMSQ($expectedResult, ...$args): void { - $result = MathTrig::SUMSQ(...$args); + $this->mightHaveException($expectedResult); + $maxRow = 0; + $funcArg = ''; + $sheet = $this->sheet; + foreach ($args as $arg) { + ++$maxRow; + $funcArg = "A1:A$maxRow"; + if ($arg !== null) { + $sheet->getCell("A$maxRow")->setValue($arg); + } + } + $sheet->getCell('B1')->setValue("=SUMSQ($funcArg)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2MY2Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2MY2Test.php index 3bf2785b..a6813bb2 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2MY2Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2MY2Test.php @@ -3,24 +3,34 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; -class SumX2MY2Test extends TestCase +class SumX2MY2Test extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUMX2MY2 * * @param mixed $expectedResult */ - public function testSUMX2MY2($expectedResult, ...$args): void + public function testSUMX2MY2($expectedResult, array $matrixData1, array $matrixData2): void { - $result = MathTrig::SUMX2MY2(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $maxRow = 0; + $funcArg1 = ''; + foreach (Functions::flattenArray($matrixData1) as $arg) { + ++$maxRow; + $funcArg1 = "A1:A$maxRow"; + $this->setCell("A$maxRow", $arg); + } + $maxRow = 0; + $funcArg2 = ''; + foreach (Functions::flattenArray($matrixData2) as $arg) { + ++$maxRow; + $funcArg2 = "C1:C$maxRow"; + $this->setCell("C$maxRow", $arg); + } + $sheet->getCell('B1')->setValue("=SUMX2MY2($funcArg1, $funcArg2)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2PY2Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2PY2Test.php index a370d79b..2db78440 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2PY2Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumX2PY2Test.php @@ -3,24 +3,34 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; -class SumX2PY2Test extends TestCase +class SumX2PY2Test extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUMX2PY2 * * @param mixed $expectedResult */ - public function testSUMX2PY2($expectedResult, ...$args): void + public function testSUMX2PY2($expectedResult, array $matrixData1, array $matrixData2): void { - $result = MathTrig::SUMX2PY2(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $maxRow = 0; + $funcArg1 = ''; + foreach (Functions::flattenArray($matrixData1) as $arg) { + ++$maxRow; + $funcArg1 = "A1:A$maxRow"; + $this->setCell("A$maxRow", $arg); + } + $maxRow = 0; + $funcArg2 = ''; + foreach (Functions::flattenArray($matrixData2) as $arg) { + ++$maxRow; + $funcArg2 = "C1:C$maxRow"; + $this->setCell("C$maxRow", $arg); + } + $sheet->getCell('B1')->setValue("=SUMX2PY2($funcArg1, $funcArg2)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumXMY2Test.php b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumXMY2Test.php index 1f64523b..eaa1ec7a 100644 --- a/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumXMY2Test.php +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/MathTrig/SumXMY2Test.php @@ -3,24 +3,34 @@ namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\MathTrig; use PhpOffice\PhpSpreadsheet\Calculation\Functions; -use PhpOffice\PhpSpreadsheet\Calculation\MathTrig; -use PHPUnit\Framework\TestCase; -class SumXMY2Test extends TestCase +class SumXMY2Test extends AllSetupTeardown { - protected function setUp(): void - { - Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL); - } - /** * @dataProvider providerSUMXMY2 * * @param mixed $expectedResult */ - public function testSUMXMY2($expectedResult, ...$args): void + public function testSUMXMY2($expectedResult, array $matrixData1, array $matrixData2): void { - $result = MathTrig::SUMXMY2(...$args); + $this->mightHaveException($expectedResult); + $sheet = $this->sheet; + $maxRow = 0; + $funcArg1 = ''; + foreach (Functions::flattenArray($matrixData1) as $arg) { + ++$maxRow; + $funcArg1 = "A1:A$maxRow"; + $this->setCell("A$maxRow", $arg); + } + $maxRow = 0; + $funcArg2 = ''; + foreach (Functions::flattenArray($matrixData2) as $arg) { + ++$maxRow; + $funcArg2 = "C1:C$maxRow"; + $this->setCell("C$maxRow", $arg); + } + $sheet->getCell('B1')->setValue("=SUMXMY2($funcArg1, $funcArg2)"); + $result = $sheet->getCell('B1')->getCalculatedValue(); self::assertEqualsWithDelta($expectedResult, $result, 1E-12); } diff --git a/tests/data/Calculation/MathTrig/ABS.php b/tests/data/Calculation/MathTrig/ABS.php index 2fc9631b..4082ceeb 100644 --- a/tests/data/Calculation/MathTrig/ABS.php +++ b/tests/data/Calculation/MathTrig/ABS.php @@ -1,12 +1,18 @@ Date: Mon, 5 Apr 2021 22:14:50 +0530 Subject: [PATCH 88/89] Unlink temporary file --- .../Writer/Xlsx/DrawingsTest.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php index c48c86db..ef2002b5 100644 --- a/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php +++ b/tests/PhpSpreadsheetTests/Writer/Xlsx/DrawingsTest.php @@ -51,24 +51,26 @@ class DrawingsTest extends AbstractFunctional public function testSaveLoadWithDrawingWithSamePath(): void { // Read spreadsheet from file - $originalFilePath = 'tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx'; + $originalFileName = 'tests/data/Writer/XLSX/saving_drawing_with_same_path.xlsx'; - $originalFile = file_get_contents($originalFilePath); + $originalFile = file_get_contents($originalFileName); - $tempFilePath = File::sysGetTempDir() . '/saving_drawing_with_same_path'; + $tempFileName = File::sysGetTempDir() . '/saving_drawing_with_same_path'; - file_put_contents($tempFilePath, $originalFile); + file_put_contents($tempFileName, $originalFile); $reader = new Xlsx(); - $spreadsheet = $reader->load($tempFilePath); + $spreadsheet = $reader->load($tempFileName); $spreadsheet->getActiveSheet()->setCellValue('D5', 'foo'); // Save spreadsheet to file to the same path. Success test case won't // throw exception here $writer = IOFactory::createWriter($spreadsheet, 'Xlsx'); - $writer->save($tempFilePath); + $writer->save($tempFileName); - $reloadedSpreadsheet = $reader->load($tempFilePath); + $reloadedSpreadsheet = $reader->load($tempFileName); + + unlink($tempFileName); // Fake assert. The only thing we need is to ensure the file is loaded without exception self::assertNotNull($reloadedSpreadsheet); From bc18fb7e77f5b5c64c872960910907deaa5cfdf2 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Tue, 6 Apr 2021 12:45:37 +0200 Subject: [PATCH 89/89] more extraction of Excel Financial functions (#1989) * More Financial function extracts, this time looking at the Periodic Cashflow functions * Initial extract of Constant Periodic Interest and Payment functions --- .../Calculation/Calculation.php | 32 +- src/PhpSpreadsheet/Calculation/Financial.php | 451 ++++++------------ .../Financial/CashFlow/Constant/Periodic.php | 190 ++++++++ .../CashFlow/Constant/Periodic/Cumulative.php | 136 ++++++ .../CashFlow/Constant/Periodic/Interest.php | 209 ++++++++ .../Periodic/InterestAndPrincipal.php | 42 ++ .../CashFlow/Constant/Periodic/Payments.php | 119 +++++ .../CashFlow/Variable/NonPeriodic.php | 20 +- .../Functions/Financial/PmtTest.php | 34 ++ .../Functions/Financial/PpmtTest.php | 31 ++ tests/data/Calculation/Financial/CUMIPMT.php | 79 ++- tests/data/Calculation/Financial/CUMPRINC.php | 84 +++- tests/data/Calculation/Financial/FV.php | 82 +++- tests/data/Calculation/Financial/IPMT.php | 93 +++- tests/data/Calculation/Financial/ISPMT.php | 70 ++- tests/data/Calculation/Financial/NPER.php | 62 ++- tests/data/Calculation/Financial/PMT.php | 52 ++ tests/data/Calculation/Financial/PPMT.php | 64 +++ tests/data/Calculation/Financial/PV.php | 36 ++ tests/data/Calculation/Financial/RATE.php | 54 +++ 20 files changed, 1530 insertions(+), 410 deletions(-) create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php create mode 100644 src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php create mode 100644 tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PpmtTest.php create mode 100644 tests/data/Calculation/Financial/PMT.php create mode 100644 tests/data/Calculation/Financial/PPMT.php diff --git a/src/PhpSpreadsheet/Calculation/Calculation.php b/src/PhpSpreadsheet/Calculation/Calculation.php index 1df103ca..9477131f 100644 --- a/src/PhpSpreadsheet/Calculation/Calculation.php +++ b/src/PhpSpreadsheet/Calculation/Calculation.php @@ -745,12 +745,12 @@ class Calculation ], 'CUMIPMT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'CUMIPMT'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'interest'], 'argumentCount' => '6', ], 'CUMPRINC' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'CUMPRINC'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Cumulative::class, 'principal'], 'argumentCount' => '6', ], 'DATE' => [ @@ -1137,12 +1137,12 @@ class Calculation ], 'FV' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'FV'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'futureValue'], 'argumentCount' => '3-5', ], 'FVSCHEDULE' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'FVSCHEDULE'], + 'functionCall' => [Financial\CashFlow\Single::class, 'futureValue'], 'argumentCount' => '2', ], 'GAMMA' => [ @@ -1434,12 +1434,12 @@ class Calculation ], 'IPMT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'IPMT'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'payment'], 'argumentCount' => '4-6', ], 'IRR' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'IRR'], + 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'rate'], 'argumentCount' => '1,2', ], 'ISBLANK' => [ @@ -1506,7 +1506,7 @@ class Calculation ], 'ISPMT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'ISPMT'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'schedulePayment'], 'argumentCount' => '4', ], 'ISREF' => [ @@ -1691,7 +1691,7 @@ class Calculation ], 'MIRR' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'MIRR'], + 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'modifiedRate'], 'argumentCount' => '3', ], 'MMULT' => [ @@ -1826,12 +1826,12 @@ class Calculation ], 'NPER' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'NPER'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'periods'], 'argumentCount' => '3-5', ], 'NPV' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'NPV'], + 'functionCall' => [Financial\CashFlow\Variable\Periodic::class, 'presentValue'], 'argumentCount' => '2+', ], 'NUMBERVALUE' => [ @@ -1893,7 +1893,7 @@ class Calculation ], 'PDURATION' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PDURATION'], + 'functionCall' => [Financial\CashFlow\Single::class, 'periods'], 'argumentCount' => '3', ], 'PEARSON' => [ @@ -1958,7 +1958,7 @@ class Calculation ], 'PMT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PMT'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'annuity'], 'argumentCount' => '3-5', ], 'POISSON' => [ @@ -1978,7 +1978,7 @@ class Calculation ], 'PPMT' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PPMT'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Payments::class, 'interestPayment'], 'argumentCount' => '4-6', ], 'PRICE' => [ @@ -2013,7 +2013,7 @@ class Calculation ], 'PV' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'PV'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic::class, 'presentValue'], 'argumentCount' => '3-5', ], 'QUARTILE' => [ @@ -2073,7 +2073,7 @@ class Calculation ], 'RATE' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'RATE'], + 'functionCall' => [Financial\CashFlow\Constant\Periodic\Interest::class, 'rate'], 'argumentCount' => '3-6', ], 'RECEIVED' => [ @@ -2140,7 +2140,7 @@ class Calculation ], 'RRI' => [ 'category' => Category::CATEGORY_FINANCIAL, - 'functionCall' => [Financial::class, 'RRI'], + 'functionCall' => [Financial\CashFlow\Single::class, 'interestRate'], 'argumentCount' => '3', ], 'RSQ' => [ diff --git a/src/PhpSpreadsheet/Calculation/Financial.php b/src/PhpSpreadsheet/Calculation/Financial.php index fde1d3da..ebf5cec1 100644 --- a/src/PhpSpreadsheet/Calculation/Financial.php +++ b/src/PhpSpreadsheet/Calculation/Financial.php @@ -16,21 +16,6 @@ class Financial const FINANCIAL_PRECISION = 1.0e-08; - private static function interestAndPrincipal($rate = 0, $per = 0, $nper = 0, $pv = 0, $fv = 0, $type = 0) - { - $pmt = self::PMT($rate, $nper, $pv, $fv, $type); - $capital = $pv; - $interest = 0; - $principal = 0; - for ($i = 1; $i <= $per; ++$i) { - $interest = ($type && $i == 1) ? 0 : -$capital * $rate; - $principal = $pmt - $interest; - $capital += $principal; - } - - return [$interest, $principal]; - } - /** * ACCRINT. * @@ -140,7 +125,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the AMORDEGRC() method in the Financial\Amortization class instead + * @see Financial\Amortization::AMORDEGRC() + * Use the AMORDEGRC() method in the Financial\Amortization class instead * * @param float $cost The cost of the asset * @param mixed $purchased Date of the purchase of the asset @@ -174,7 +160,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the AMORLINC() method in the Financial\Amortization class instead + * @see Financial\Amortization::AMORLINC() + * Use the AMORLINC() method in the Financial\Amortization class instead * * @param float $cost The cost of the asset * @param mixed $purchased Date of the purchase of the asset @@ -206,7 +193,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the COUPDAYBS() method in the Financial\Coupons class instead + * @see Financial\Coupons::COUPDAYBS() + * Use the COUPDAYBS() method in the Financial\Coupons class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue @@ -242,7 +230,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the COUPDAYS() method in the Financial\Coupons class instead + * @see Financial\Coupons::COUPDAYS() + * Use the COUPDAYS() method in the Financial\Coupons class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue @@ -278,7 +267,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the COUPDAYSNC() method in the Financial\Coupons class instead + * @see Financial\Coupons::COUPDAYSNC() + * Use the COUPDAYSNC() method in the Financial\Coupons class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue @@ -314,7 +304,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the COUPNCD() method in the Financial\Coupons class instead + * @see Financial\Coupons::COUPNCD() + * Use the COUPNCD() method in the Financial\Coupons class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue @@ -352,7 +343,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the COUPNUM() method in the Financial\Coupons class instead + * @see Financial\Coupons::COUPNUM() + * Use the COUPNUM() method in the Financial\Coupons class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue @@ -388,7 +380,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the COUPPCD() method in the Financial\Coupons class instead + * @see Financial\Coupons::COUPPCD() + * Use the COUPPCD() method in the Financial\Coupons class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue @@ -423,47 +416,26 @@ class Financial * Excel Function: * CUMIPMT(rate,nper,pv,start,end[,type]) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic\Cumulative::interest() + * Use the interest() method in the Financial\CashFlow\Constant\Periodic\Cumulative class instead + * * @param float $rate The Interest rate * @param int $nper The total number of payment periods * @param float $pv Present Value * @param int $start The first period in the calculation. - * Payment periods are numbered beginning with 1. + * Payment periods are numbered beginning with 1. * @param int $end the last period in the calculation * @param int $type A number 0 or 1 and indicates when payments are due: - * 0 or omitted At the end of the period. - * 1 At the beginning of the period. + * 0 or omitted At the end of the period. + * 1 At the beginning of the period. * * @return float|string */ public static function CUMIPMT($rate, $nper, $pv, $start, $end, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $nper = (int) Functions::flattenSingleValue($nper); - $pv = Functions::flattenSingleValue($pv); - $start = (int) Functions::flattenSingleValue($start); - $end = (int) Functions::flattenSingleValue($end); - $type = (int) Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - if ($start < 1 || $start > $end) { - return Functions::VALUE(); - } - - // Calculate - $interest = 0; - for ($per = $start; $per <= $end; ++$per) { - $ipmt = self::IPMT($rate, $per, $nper, $pv, 0, $type); - if (is_string($ipmt)) { - return $ipmt; - } - - $interest += $ipmt; - } - - return $interest; + return Financial\CashFlow\Constant\Periodic\Cumulative::interest($rate, $nper, $pv, $start, $end, $type); } /** @@ -474,47 +446,26 @@ class Financial * Excel Function: * CUMPRINC(rate,nper,pv,start,end[,type]) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic\Cumulative::principal() + * Use the principal() method in the Financial\CashFlow\Constant\Periodic\Cumulative class instead + * * @param float $rate The Interest rate * @param int $nper The total number of payment periods * @param float $pv Present Value * @param int $start The first period in the calculation. - * Payment periods are numbered beginning with 1. + * Payment periods are numbered beginning with 1. * @param int $end the last period in the calculation * @param int $type A number 0 or 1 and indicates when payments are due: - * 0 or omitted At the end of the period. - * 1 At the beginning of the period. + * 0 or omitted At the end of the period. + * 1 At the beginning of the period. * * @return float|string */ public static function CUMPRINC($rate, $nper, $pv, $start, $end, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $nper = (int) Functions::flattenSingleValue($nper); - $pv = Functions::flattenSingleValue($pv); - $start = (int) Functions::flattenSingleValue($start); - $end = (int) Functions::flattenSingleValue($end); - $type = (int) Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - if ($start < 1 || $start > $end) { - return Functions::VALUE(); - } - - // Calculate - $principal = 0; - for ($per = $start; $per <= $end; ++$per) { - $ppmt = self::PPMT($rate, $per, $nper, $pv, 0, $type); - if (is_string($ppmt)) { - return $ppmt; - } - - $principal += $ppmt; - } - - return $principal; + return Financial\CashFlow\Constant\Periodic\Cumulative::principal($rate, $nper, $pv, $start, $end, $type); } /** @@ -532,7 +483,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the DB() method in the Financial\Depreciation class instead + * @see Financial\Depreciation::DB() + * Use the DB() method in the Financial\Depreciation class instead * * @param float $cost Initial cost of the asset * @param float $salvage Value at the end of the depreciation. @@ -562,7 +514,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the DDB() method in the Financial\Depreciation class instead + * @see Financial\Depreciation::DDB() + * Use the DDB() method in the Financial\Depreciation class instead * * @param float $cost Initial cost of the asset * @param float $salvage Value at the end of the depreciation. @@ -646,7 +599,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the decimal() method in the Financial\Dollar class instead + * @see Financial\Dollar::decimal() + * Use the decimal() method in the Financial\Dollar class instead * * @param float $fractional_dollar Fractional Dollar * @param int $fraction Fraction @@ -670,7 +624,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the fractional() method in the Financial\Dollar class instead + * @see Financial\Dollar::fractional() + * Use the fractional() method in the Financial\Dollar class instead * * @param float $decimal_dollar Decimal Dollar * @param int $fraction Fraction @@ -693,7 +648,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the effective() method in the Financial\InterestRate class instead + * @see Financial\InterestRate::effective() + * Use the effective() method in the Financial\InterestRate class instead * * @param float $nominalRate Nominal interest rate * @param int $periodsPerYear Number of compounding payments per year @@ -713,6 +669,11 @@ class Financial * Excel Function: * FV(rate,nper,pmt[,pv[,type]]) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic::futureValue() + * Use the futureValue() method in the Financial\CashFlow\Constant\Periodic class instead + * * @param float $rate The interest rate per period * @param int $nper Total number of payment periods in an annuity * @param float $pmt The payment made each period: it cannot change over the @@ -728,23 +689,7 @@ class Financial */ public static function FV($rate = 0, $nper = 0, $pmt = 0, $pv = 0, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $nper = Functions::flattenSingleValue($nper); - $pmt = Functions::flattenSingleValue($pmt); - $pv = Functions::flattenSingleValue($pv); - $type = Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - - // Calculate - if ($rate !== null && $rate != 0) { - return -$pv * (1 + $rate) ** $nper - $pmt * (1 + $rate * $type) * ((1 + $rate) ** $nper - 1) / $rate; - } - - return -$pv - $pmt * $nper; + return Financial\CashFlow\Constant\Periodic::futureValue($rate, $nper, $pmt, $pv, $type); } /** @@ -825,11 +770,17 @@ class Financial /** * IPMT. * - * Returns the interest payment for a given period for an investment based on periodic, constant payments and a constant interest rate. + * Returns the interest payment for a given period for an investment based on periodic, constant payments + * and a constant interest rate. * * Excel Function: * IPMT(rate,per,nper,pv[,fv][,type]) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic\Interest::payment() + * Use the payment() method in the Financial\CashFlow\Constant\Periodic class instead + * * @param float $rate Interest rate per period * @param int $per Period for which we want to find the interest * @param int $nper Number of periods @@ -841,25 +792,7 @@ class Financial */ public static function IPMT($rate, $per, $nper, $pv, $fv = 0, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $per = (int) Functions::flattenSingleValue($per); - $nper = (int) Functions::flattenSingleValue($nper); - $pv = Functions::flattenSingleValue($pv); - $fv = Functions::flattenSingleValue($fv); - $type = (int) Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - if ($per <= 0 || $per > $nper) { - return Functions::NAN(); - } - - // Calculate - $interestAndPrincipal = self::interestAndPrincipal($rate, $per, $nper, $pv, $fv, $type); - - return $interestAndPrincipal[0]; + return Financial\CashFlow\Constant\Periodic\Interest::payment($rate, $per, $nper, $pv, $fv, $type); } /** @@ -876,6 +809,9 @@ class Financial * * @Deprecated 1.18.0 * + * @see Financial\CashFlow\Variable\Periodic::rate() + * Use the rate() method in the Financial\CashFlow\Variable\Periodic class instead + * * @param mixed $values An array or a reference to cells that contain numbers for which you want * to calculate the internal rate of return. * Values must contain at least one positive value and one negative value to @@ -883,9 +819,6 @@ class Financial * @param mixed $guess A number that you guess is close to the result of IRR * * @return float|string - * - *@see Financial\CashFlow\Variable\Periodic::rate() - * Use the IRR() method in the Financial\CashFlow\Variable\Periodic class instead */ public static function IRR($values, $guess = 0.1) { @@ -898,7 +831,12 @@ class Financial * Returns the interest payment for an investment based on an interest rate and a constant payment schedule. * * Excel Function: - * =ISPMT(interest_rate, period, number_payments, PV) + * =ISPMT(interest_rate, period, number_payments, pv) + * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic\Interest::schedulePayment() + * Use the schedulePayment() method in the Financial\CashFlow\Constant\Periodic class instead * * interest_rate is the interest rate for the investment * @@ -906,32 +844,11 @@ class Financial * * number_payments is the number of payments for the annuity * - * PV is the loan amount or present value of the payments + * pv is the loan amount or present value of the payments */ public static function ISPMT(...$args) { - // Return value - $returnValue = 0; - - // Get the parameters - $aArgs = Functions::flattenArray($args); - $interestRate = array_shift($aArgs); - $period = array_shift($aArgs); - $numberPeriods = array_shift($aArgs); - $principleRemaining = array_shift($aArgs); - - // Calculate - $principlePayment = ($principleRemaining * 1.0) / ($numberPeriods * 1.0); - for ($i = 0; $i <= $period; ++$i) { - $returnValue = $interestRate * $principleRemaining * -1; - $principleRemaining -= $principlePayment; - // principle needs to be 0 after the last payment, don't let floating point screw it up - if ($i == $numberPeriods) { - $returnValue = 0; - } - } - - return $returnValue; + return Financial\CashFlow\Constant\Periodic\Interest::schedulePayment(...$args); } /** @@ -946,7 +863,7 @@ class Financial * @Deprecated 1.18.0 * * @see Financial\CashFlow\Variable\Periodic::modifiedRate() - * Use the MIRR() method in the Financial\CashFlow\Variable\Periodic class instead + * Use the modifiedRate() method in the Financial\CashFlow\Variable\Periodic class instead * * @param mixed $values An array or a reference to cells that contain a series of payments and * income occurring at regular intervals. @@ -971,7 +888,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the nominal() method in the Financial\InterestRate class instead + * @see Financial\InterestRate::nominal() + * Use the nominal() method in the Financial\InterestRate class instead * * @param float $effectiveRate Effective interest rate * @param int $periodsPerYear Number of compounding payments per year @@ -988,6 +906,8 @@ class Financial * * Returns the number of periods for a cash flow with constant periodic payments (annuities), and interest rate. * + * @Deprecated 1.18.0 + * * @param float $rate Interest rate per period * @param int $pmt Periodic payment (annuity) * @param float $pv Present Value @@ -995,33 +915,13 @@ class Financial * @param int $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period * * @return float|string Result, or a string containing an error + * + *@see Financial\CashFlow\Constant\Periodic::periods() + * Use the periods() method in the Financial\CashFlow\Constant\Periodic class instead */ public static function NPER($rate = 0, $pmt = 0, $pv = 0, $fv = 0, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $pmt = Functions::flattenSingleValue($pmt); - $pv = Functions::flattenSingleValue($pv); - $fv = Functions::flattenSingleValue($fv); - $type = Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - - // Calculate - if ($rate !== null && $rate != 0) { - if ($pmt == 0 && $pv == 0) { - return Functions::NAN(); - } - - return log(($pmt * (1 + $rate * $type) / $rate - $fv) / ($pv + $pmt * (1 + $rate * $type) / $rate)) / log(1 + $rate); - } - if ($pmt == 0) { - return Functions::NAN(); - } - - return (-$pv - $fv) / $pmt; + return Financial\CashFlow\Constant\Periodic::periods($rate, $pmt, $pv, $fv, $type); } /** @@ -1031,10 +931,10 @@ class Financial * * @Deprecated 1.18.0 * - * @return float + * @see Financial\CashFlow\Variable\Periodic::presentValue() + * Use the presentValue() method in the Financial\CashFlow\Variable\Periodic class instead * - *@see Financial\CashFlow\Variable\Periodic::presentValue() - * Use the NPV() method in the Financial\CashFlow\Variable\Periodic class instead + * @return float */ public static function NPV(...$args) { @@ -1067,6 +967,11 @@ class Financial * * Returns the constant payment (annuity) for a cash flow with a constant interest rate. * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic\Payments::annuity() + * Use the annuity() method in the Financial\CashFlow\Constant\Periodic\Payments class instead + * * @param float $rate Interest rate per period * @param int $nper Number of periods * @param float $pv Present Value @@ -1077,29 +982,19 @@ class Financial */ public static function PMT($rate = 0, $nper = 0, $pv = 0, $fv = 0, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $nper = Functions::flattenSingleValue($nper); - $pv = Functions::flattenSingleValue($pv); - $fv = Functions::flattenSingleValue($fv); - $type = Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - - // Calculate - if ($rate !== null && $rate != 0) { - return (-$fv - $pv * (1 + $rate) ** $nper) / (1 + $rate * $type) / (((1 + $rate) ** $nper - 1) / $rate); - } - - return (-$pv - $fv) / $nper; + return Financial\CashFlow\Constant\Periodic\Payments::annuity($rate, $nper, $pv, $fv, $type); } /** * PPMT. * - * Returns the interest payment for a given period for an investment based on periodic, constant payments and a constant interest rate. + * Returns the interest payment for a given period for an investment based on periodic, constant payments + * and a constant interest rate. + * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic\Payments::interestPayment() + * Use the interestPayment() method in the Financial\CashFlow\Constant\Periodic\Payments class instead * * @param float $rate Interest rate per period * @param int $per Period for which we want to find the interest @@ -1112,25 +1007,7 @@ class Financial */ public static function PPMT($rate, $per, $nper, $pv, $fv = 0, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $per = (int) Functions::flattenSingleValue($per); - $nper = (int) Functions::flattenSingleValue($nper); - $pv = Functions::flattenSingleValue($pv); - $fv = Functions::flattenSingleValue($fv); - $type = (int) Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - if ($per <= 0 || $per > $nper) { - return Functions::NAN(); - } - - // Calculate - $interestAndPrincipal = self::interestAndPrincipal($rate, $per, $nper, $pv, $fv, $type); - - return $interestAndPrincipal[1]; + return Financial\CashFlow\Constant\Periodic\Payments::interestPayment($rate, $per, $nper, $pv, $fv, $type); } /** @@ -1140,7 +1017,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the price() method in the Financial\Securities\Price class instead + * @see Financial\Securities\Price::price() + * Use the price() method in the Financial\Securities\Price class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue date when the security @@ -1175,7 +1053,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the priceDiscounted() method in the Financial\Securities\Price class instead + * @see Financial\Securities\Price::priceDiscounted() + * Use the priceDiscounted() method in the Financial\Securities\Price class instead * * @param mixed $settlement The security's settlement date. * The security settlement date is the date after the issue date when the security @@ -1205,7 +1084,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the priceAtMaturity() method in the Financial\Securities\Price class instead + * @see Financial\Securities\Price::priceAtMaturity() + * Use the priceAtMaturity() method in the Financial\Securities\Price class instead * * @param mixed $settlement The security's settlement date. * The security's settlement date is the date after the issue date when the security @@ -1234,6 +1114,11 @@ class Financial * * Returns the Present Value of a cash flow with constant payments and interest rate (annuities). * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic::presentValue() + * Use the presentValue() method in the Financial\CashFlow\Constant\Periodic class instead + * * @param float $rate Interest rate per period * @param int $nper Number of periods * @param float $pmt Periodic payment (annuity) @@ -1244,23 +1129,7 @@ class Financial */ public static function PV($rate = 0, $nper = 0, $pmt = 0, $fv = 0, $type = 0) { - $rate = Functions::flattenSingleValue($rate); - $nper = Functions::flattenSingleValue($nper); - $pmt = Functions::flattenSingleValue($pmt); - $fv = Functions::flattenSingleValue($fv); - $type = Functions::flattenSingleValue($type); - - // Validate parameters - if ($type != 0 && $type != 1) { - return Functions::NAN(); - } - - // Calculate - if ($rate !== null && $rate != 0) { - return (-$pmt * (1 + $rate * $type) * (((1 + $rate) ** $nper - 1) / $rate) - $fv) / (1 + $rate) ** $nper; - } - - return -$fv - $pmt * $nper; + return Financial\CashFlow\Constant\Periodic::presentValue($rate, $nper, $pmt, $fv, $type); } /** @@ -1274,6 +1143,11 @@ class Financial * Excel Function: * RATE(nper,pmt,pv[,fv[,type[,guess]]]) * + * @Deprecated 1.18.0 + * + * @see Financial\CashFlow\Constant\Periodic\Interest::rate() + * Use the rate() method in the Financial\CashFlow\Constant\Periodic class instead + * * @param mixed $nper The total number of payment periods in an annuity * @param mixed $pmt The payment made each period and cannot change over the life * of the annuity. @@ -1294,47 +1168,7 @@ class Financial */ public static function RATE($nper, $pmt, $pv, $fv = 0.0, $type = 0, $guess = 0.1) { - $nper = (int) Functions::flattenSingleValue($nper); - $pmt = Functions::flattenSingleValue($pmt); - $pv = Functions::flattenSingleValue($pv); - $fv = ($fv === null) ? 0.0 : Functions::flattenSingleValue($fv); - $type = ($type === null) ? 0 : (int) Functions::flattenSingleValue($type); - $guess = ($guess === null) ? 0.1 : Functions::flattenSingleValue($guess); - - $rate = $guess; - // rest of code adapted from python/numpy - $close = false; - $iter = 0; - while (!$close && $iter < self::FINANCIAL_MAX_ITERATIONS) { - $nextdiff = self::rateNextGuess($rate, $nper, $pmt, $pv, $fv, $type); - if (!is_numeric($nextdiff)) { - break; - } - $rate1 = $rate - $nextdiff; - $close = abs($rate1 - $rate) < self::FINANCIAL_PRECISION; - ++$iter; - $rate = $rate1; - } - - return $close ? $rate : Functions::NAN(); - } - - private static function rateNextGuess($rate, $nper, $pmt, $pv, $fv, $type) - { - if ($rate == 0) { - return Functions::NAN(); - } - $tt1 = ($rate + 1) ** $nper; - $tt2 = ($rate + 1) ** ($nper - 1); - $numerator = $fv + $tt1 * $pv + $pmt * ($tt1 - 1) * ($rate * $type + 1) / $rate; - $denominator = $nper * $tt2 * $pv - $pmt * ($tt1 - 1) * ($rate * $type + 1) / ($rate * $rate) - + $nper * $pmt * $tt2 * ($rate * $type + 1) / $rate - + $pmt * ($tt1 - 1) * $type / $rate; - if ($denominator == 0) { - return Functions::NAN(); - } - - return $numerator / $denominator; + return Financial\CashFlow\Constant\Periodic\Interest::rate($nper, $pmt, $pv, $fv, $type, $guess); } /** @@ -1343,17 +1177,18 @@ class Financial * Returns the price per $100 face value of a discounted security. * * @param mixed $settlement The security's settlement date. - * The security settlement date is the date after the issue date when the security is traded to the buyer. + * The security settlement date is the date after the issue date when the security + * is traded to the buyer. * @param mixed $maturity The security's maturity date. - * The maturity date is the date when the security expires. + * The maturity date is the date when the security expires. * @param mixed $investment The amount invested in the security * @param mixed $discount The security's discount rate * @param mixed $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string Result, or a string containing an error */ @@ -1410,7 +1245,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the SLN() method in the Financial\Depreciation class instead + * @see Financial\Depreciation::SLN() + * Use the SLN() method in the Financial\Depreciation class instead * * @param mixed $cost Initial cost of the asset * @param mixed $salvage Value at the end of the depreciation @@ -1430,7 +1266,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the SYD() method in the Financial\Depreciation class instead + * @see Financial\Depreciation::SYD() + * Use the SYD() method in the Financial\Depreciation class instead * * @param mixed $cost Initial cost of the asset * @param mixed $salvage Value at the end of the depreciation @@ -1451,10 +1288,12 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the bondEquivalentYield() method in the Financial\TreasuryBill class instead + * @see Financial\TreasuryBill::bondEquivalentYield() + * Use the bondEquivalentYield() method in the Financial\TreasuryBill class instead * * @param mixed $settlement The Treasury bill's settlement date. - * The Treasury bill's settlement date is the date after the issue date when the Treasury bill is traded to the buyer. + * The Treasury bill's settlement date is the date after the issue date when the + * Treasury bill is traded to the buyer. * @param mixed $maturity The Treasury bill's maturity date. * The maturity date is the date when the Treasury bill expires. * @param int $discount The Treasury bill's discount rate @@ -1473,7 +1312,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the price() method in the Financial\TreasuryBill class instead + * @see Financial\TreasuryBill::price() + * Use the price() method in the Financial\TreasuryBill class instead * * @param mixed $settlement The Treasury bill's settlement date. * The Treasury bill's settlement date is the date after the issue date @@ -1496,7 +1336,8 @@ class Financial * * @Deprecated 1.18.0 * - * @see Use the yield() method in the Financial\TreasuryBill class instead + * @see Financial\TreasuryBill::yield() + * Use the yield() method in the Financial\TreasuryBill class instead * * @param mixed $settlement The Treasury bill's settlement date. * The Treasury bill's settlement date is the date after the issue date @@ -1554,13 +1395,15 @@ class Financial * Use the presentValue() method in the Financial\CashFlow\Variable\NonPeriodic class instead * * @param float $rate the discount rate to apply to the cash flows - * @param float[] $values A series of cash flows that corresponds to a schedule of payments in dates. - * The first payment is optional and corresponds to a cost or payment that occurs at the beginning of the investment. - * If the first value is a cost or payment, it must be a negative value. All succeeding payments are discounted based on a 365-day year. - * The series of values must contain at least one positive value and one negative value. - * @param mixed[] $dates A schedule of payment dates that corresponds to the cash flow payments. - * 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[] $values A series of cash flows that corresponds to a schedule of payments in dates. + * The first payment is optional and corresponds to a cost or payment that occurs + * at the beginning of the investment. + * If the first value is a cost or payment, it must be a negative value. + * All succeeding payments are discounted based on a 365-day year. + * The series of values must contain at least one positive value and one negative value. + * @param mixed[] $dates A schedule of payment dates that corresponds to the cash flow payments. + * 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. * * @return float|mixed|string */ @@ -1619,11 +1462,11 @@ class Financial * @param int $rate The security's interest rate at date of issue * @param int $price The security's price per $100 face value * @param int $basis The type of day count to use. - * 0 or omitted US (NASD) 30/360 - * 1 Actual/actual - * 2 Actual/360 - * 3 Actual/365 - * 4 European 30/360 + * 0 or omitted US (NASD) 30/360 + * 1 Actual/actual + * 2 Actual/360 + * 3 Actual/365 + * 4 European 30/360 * * @return float|string Result, or a string containing an error */ diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php new file mode 100644 index 00000000..39a51875 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic.php @@ -0,0 +1,190 @@ +getMessage(); + } + + // Validate parameters + if ($numberOfPeriods < 0 || ($type !== 0 && $type !== 1)) { + return Functions::NAN(); + } + + return self::calculateFutureValue($rate, $numberOfPeriods, $payment, $presentValue, $type); + } + + /** + * PV. + * + * Returns the Present Value of a cash flow with constant payments and interest rate (annuities). + * + * @param mixed $rate Interest rate per period + * @param mixed $numberOfPeriods Number of periods as an integer + * @param mixed $payment Periodic payment (annuity) + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function presentValue($rate, $numberOfPeriods, $payment = 0, $futureValue = 0, $type = 0) + { + $rate = Functions::flattenSingleValue($rate); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $payment = ($payment === null) ? 0.0 : Functions::flattenSingleValue($payment); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? 0 : Functions::flattenSingleValue($type); + + try { + $rate = self::validateFloat($rate); + $numberOfPeriods = self::validateInt($numberOfPeriods); + $payment = self::validateFloat($payment); + $futureValue = self::validateFloat($futureValue); + $type = self::validateInt($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($numberOfPeriods < 0 || ($type !== 0 && $type !== 1)) { + return Functions::NAN(); + } + + return self::calculatePresentValue($rate, $numberOfPeriods, $payment, $futureValue, $type); + } + + /** + * NPER. + * + * Returns the number of periods for a cash flow with constant periodic payments (annuities), and interest rate. + * + * @param mixed $rate Interest rate per period + * @param mixed $payment Periodic payment (annuity) + * @param mixed $presentValue Present Value + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function periods($rate, $payment, $presentValue, $futureValue = 0, $type = 0) + { + $rate = Functions::flattenSingleValue($rate); + $payment = Functions::flattenSingleValue($payment); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? 0 : Functions::flattenSingleValue($type); + + try { + $rate = self::validateFloat($rate); + $payment = self::validateFloat($payment); + $presentValue = self::validateFloat($presentValue); + $futureValue = self::validateFloat($futureValue); + $type = self::validateInt($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($payment == 0.0 || ($type != 0 && $type != 1)) { + return Functions::NAN(); + } + + return self::calculatePeriods($rate, $payment, $presentValue, $futureValue, $type); + } + + private static function calculateFutureValue( + float $rate, + int $numberOfPeriods, + float $payment, + float $presentValue, + int $type + ): float { + if ($rate !== null && $rate != 0) { + return -$presentValue * + (1 + $rate) ** $numberOfPeriods - $payment * (1 + $rate * $type) * ((1 + $rate) ** $numberOfPeriods - 1) + / $rate; + } + + return -$presentValue - $payment * $numberOfPeriods; + } + + private static function calculatePresentValue( + float $rate, + int $numberOfPeriods, + float $payment, + float $futureValue, + int $type + ): float { + if ($rate != 0.0) { + return (-$payment * (1 + $rate * $type) + * (((1 + $rate) ** $numberOfPeriods - 1) / $rate) - $futureValue) / (1 + $rate) ** $numberOfPeriods; + } + + return -$futureValue - $payment * $numberOfPeriods; + } + + /** + * @return float|string + */ + private static function calculatePeriods( + float $rate, + float $payment, + float $presentValue, + float $futureValue, + int $type + ) { + if ($rate != 0.0) { + if ($presentValue == 0.0) { + return Functions::NAN(); + } + + return log(($payment * (1 + $rate * $type) / $rate - $futureValue) / + ($presentValue + $payment * (1 + $rate * $type) / $rate)) / log(1 + $rate); + } + + return (-$presentValue - $futureValue) / $payment; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php new file mode 100644 index 00000000..1e05a446 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Cumulative.php @@ -0,0 +1,136 @@ +getMessage(); + } + + // Validate parameters + if ($type !== 0 && $type !== 1) { + return Functions::NAN(); + } + if ($start < 1 || $start > $end) { + return Functions::NAN(); + } + + // Calculate + $interest = 0; + for ($per = $start; $per <= $end; ++$per) { + $ipmt = Financial::IPMT($rate, $per, $periods, $presentValue, 0, $type); + if (is_string($ipmt)) { + return $ipmt; + } + + $interest += $ipmt; + } + + return $interest; + } + + /** + * CUMPRINC. + * + * Returns the cumulative principal paid on a loan between the start and end periods. + * + * Excel Function: + * CUMPRINC(rate,nper,pv,start,end[,type]) + * + * @param mixed $rate The Interest rate + * @param mixed $periods The total number of payment periods as an integer + * @param mixed $presentValue Present Value + * @param mixed $start The first period in the calculation. + * Payment periods are numbered beginning with 1. + * @param mixed $end the last period in the calculation + * @param mixed $type A number 0 or 1 and indicates when payments are due: + * 0 or omitted At the end of the period. + * 1 At the beginning of the period. + * + * @return float|string + */ + public static function principal($rate, $periods, $presentValue, $start, $end, $type = 0) + { + $rate = Functions::flattenSingleValue($rate); + $periods = Functions::flattenSingleValue($periods); + $presentValue = Functions::flattenSingleValue($presentValue); + $start = Functions::flattenSingleValue($start); + $end = Functions::flattenSingleValue($end); + $type = ($type === null) ? 0 : Functions::flattenSingleValue($type); + + try { + $rate = self::validateFloat($rate); + $periods = self::validateInt($periods); + $presentValue = self::validateFloat($presentValue); + $start = self::validateInt($start); + $end = self::validateInt($end); + $type = self::validateInt($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($type !== 0 && $type !== 1) { + return Functions::NAN(); + } + if ($start < 1 || $start > $end) { + return Functions::VALUE(); + } + + // Calculate + $principal = 0; + for ($per = $start; $per <= $end; ++$per) { + $ppmt = Payments::interestPayment($rate, $per, $periods, $presentValue, 0, $type); + if (is_string($ppmt)) { + return $ppmt; + } + + $principal += $ppmt; + } + + return $principal; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php new file mode 100644 index 00000000..1bb63a2a --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Interest.php @@ -0,0 +1,209 @@ +getMessage(); + } + + // Validate parameters + if ($type != 0 && $type != 1) { + return Functions::NAN(); + } + if ($period <= 0 || $period > $numberOfPeriods) { + return Functions::NAN(); + } + + // Calculate + $interestAndPrincipal = new InterestAndPrincipal( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue, + $type + ); + + return $interestAndPrincipal->interest(); + } + + /** + * ISPMT. + * + * Returns the interest payment for an investment based on an interest rate and a constant payment schedule. + * + * Excel Function: + * =ISPMT(interest_rate, period, number_payments, pv) + * + * interest_rate is the interest rate for the investment + * + * period is the period to calculate the interest rate. It must be betweeen 1 and number_payments. + * + * number_payments is the number of payments for the annuity + * + * pv is the loan amount or present value of the payments + */ + public static function schedulePayment($interestRate, $period, $numberOfPeriods, $principleRemaining) + { + $interestRate = Functions::flattenSingleValue($interestRate); + $period = Functions::flattenSingleValue($period); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $principleRemaining = Functions::flattenSingleValue($principleRemaining); + + try { + $interestRate = self::validateFloat($interestRate); + $period = self::validateInt($period); + $numberOfPeriods = self::validateInt($numberOfPeriods); + $principleRemaining = self::validateFloat($principleRemaining); + } catch (Exception $e) { + return $e->getMessage(); + } + + if ($period <= 0 || $period > $numberOfPeriods) { + return Functions::NAN(); + } + // Return value + $returnValue = 0; + + // Calculate + $principlePayment = ($principleRemaining * 1.0) / ($numberOfPeriods * 1.0); + for ($i = 0; $i <= $period; ++$i) { + $returnValue = $interestRate * $principleRemaining * -1; + $principleRemaining -= $principlePayment; + // principle needs to be 0 after the last payment, don't let floating point screw it up + if ($i == $numberOfPeriods) { + $returnValue = 0.0; + } + } + + return $returnValue; + } + + /** + * RATE. + * + * Returns the interest rate per period of an annuity. + * RATE is calculated by iteration and can have zero or more solutions. + * If the successive results of RATE do not converge to within 0.0000001 after 20 iterations, + * RATE returns the #NUM! error value. + * + * Excel Function: + * RATE(nper,pmt,pv[,fv[,type[,guess]]]) + * + * @param mixed $numberOfPeriods The total number of payment periods in an annuity + * @param mixed $payment The payment made each period and cannot change over the life of the annuity. + * Typically, pmt includes principal and interest but no other fees or taxes. + * @param mixed $presentValue The present value - the total amount that a series of future payments is worth now + * @param mixed $futureValue The future value, or a cash balance you want to attain after the last payment is made. + * If fv is omitted, it is assumed to be 0 (the future value of a loan, + * for example, is 0). + * @param mixed $type A number 0 or 1 and indicates when payments are due: + * 0 or omitted At the end of the period. + * 1 At the beginning of the period. + * @param mixed $guess Your guess for what the rate will be. + * If you omit guess, it is assumed to be 10 percent. + * + * @return float|string + */ + public static function rate($numberOfPeriods, $payment, $presentValue, $futureValue = 0.0, $type = 0, $guess = 0.1) + { + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $payment = Functions::flattenSingleValue($payment); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? 0 : Functions::flattenSingleValue($type); + $guess = ($guess === null) ? 0.1 : Functions::flattenSingleValue($guess); + + try { + $numberOfPeriods = self::validateInt($numberOfPeriods); + $payment = self::validateFloat($payment); + $presentValue = self::validateFloat($presentValue); + $futureValue = self::validateFloat($futureValue); + $type = self::validateInt($type); + $guess = self::validateFloat($guess); + } catch (Exception $e) { + return $e->getMessage(); + } + + $rate = $guess; + // rest of code adapted from python/numpy + $close = false; + $iter = 0; + while (!$close && $iter < self::FINANCIAL_MAX_ITERATIONS) { + $nextdiff = self::rateNextGuess($rate, $numberOfPeriods, $payment, $presentValue, $futureValue, $type); + if (!is_numeric($nextdiff)) { + break; + } + $rate1 = $rate - $nextdiff; + $close = abs($rate1 - $rate) < self::FINANCIAL_PRECISION; + ++$iter; + $rate = $rate1; + } + + return $close ? $rate : Functions::NAN(); + } + + private static function rateNextGuess($rate, $nper, $pmt, $pv, $fv, $type) + { + if ($rate == 0) { + return Functions::NAN(); + } + $tt1 = ($rate + 1) ** $nper; + $tt2 = ($rate + 1) ** ($nper - 1); + $numerator = $fv + $tt1 * $pv + $pmt * ($tt1 - 1) * ($rate * $type + 1) / $rate; + $denominator = $nper * $tt2 * $pv - $pmt * ($tt1 - 1) * ($rate * $type + 1) / ($rate * $rate) + + $nper * $pmt * $tt2 * ($rate * $type + 1) / $rate + + $pmt * ($tt1 - 1) * $type / $rate; + if ($denominator == 0) { + return Functions::NAN(); + } + + return $numerator / $denominator; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php new file mode 100644 index 00000000..5e76f346 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/InterestAndPrincipal.php @@ -0,0 +1,42 @@ +interest = $interest; + $this->principal = $principal; + } + + public function interest(): float + { + return $this->interest; + } + + public function principal(): float + { + return $this->principal; + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php new file mode 100644 index 00000000..a0c61586 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Constant/Periodic/Payments.php @@ -0,0 +1,119 @@ +getMessage(); + } + + // Validate parameters + if ($type != 0 && $type != 1) { + return Functions::NAN(); + } + + // Calculate + if ($interestRate != 0.0) { + return (-$futureValue - $presentValue * (1 + $interestRate) ** $numberOfPeriods) / + (1 + $interestRate * $type) / (((1 + $interestRate) ** $numberOfPeriods - 1) / $interestRate); + } + + return (-$presentValue - $futureValue) / $numberOfPeriods; + } + + /** + * PPMT. + * + * Returns the interest payment for a given period for an investment based on periodic, constant payments + * and a constant interest rate. + * + * @param mixed $interestRate Interest rate per period + * @param mixed $period Period for which we want to find the interest + * @param mixed $numberOfPeriods Number of periods + * @param mixed $presentValue Present Value + * @param mixed $futureValue Future Value + * @param mixed $type Payment type: 0 = at the end of each period, 1 = at the beginning of each period + * + * @return float|string Result, or a string containing an error + */ + public static function interestPayment( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue = 0, + $type = 0 + ) { + $interestRate = Functions::flattenSingleValue($interestRate); + $period = Functions::flattenSingleValue($period); + $numberOfPeriods = Functions::flattenSingleValue($numberOfPeriods); + $presentValue = Functions::flattenSingleValue($presentValue); + $futureValue = ($futureValue === null) ? 0.0 : Functions::flattenSingleValue($futureValue); + $type = ($type === null) ? 0 : Functions::flattenSingleValue($type); + + try { + $interestRate = self::validateFloat($interestRate); + $period = self::validateInt($period); + $numberOfPeriods = self::validateInt($numberOfPeriods); + $presentValue = self::validateFloat($presentValue); + $futureValue = self::validateFloat($futureValue); + $type = self::validateInt($type); + } catch (Exception $e) { + return $e->getMessage(); + } + + // Validate parameters + if ($type != 0 && $type != 1) { + return Functions::NAN(); + } + if ($period <= 0 || $period > $numberOfPeriods) { + return Functions::NAN(); + } + + // Calculate + $interestAndPrincipal = new InterestAndPrincipal( + $interestRate, + $period, + $numberOfPeriods, + $presentValue, + $futureValue, + $type + ); + + return $interestAndPrincipal->principal(); + } +} diff --git a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php index 58f8fdaf..40df776f 100644 --- a/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php +++ b/src/PhpSpreadsheet/Calculation/Financial/CashFlow/Variable/NonPeriodic.php @@ -178,14 +178,12 @@ class NonPeriodic $valCount = count($values); try { + self::validateXnpv($rate, $values, $dates); $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); } catch (Exception $e) { return $e->getMessage(); } - $rslt = self::validateXnpv($rate, $values, $dates); - if ($rslt) { - return $rslt; - } + $xnpv = 0.0; for ($i = 0; $i < $valCount; ++$i) { if (!is_numeric($values[$i])) { @@ -212,23 +210,17 @@ class NonPeriodic return is_finite($xnpv) ? $xnpv : Functions::VALUE(); } - private static function validateXnpv($rate, $values, $dates) + private static function validateXnpv($rate, $values, $dates): void { if (!is_numeric($rate)) { - return Functions::VALUE(); + throw new Exception(Functions::VALUE()); } $valCount = count($values); if ($valCount != count($dates)) { - return Functions::NAN(); + throw new Exception(Functions::NAN()); } if ($valCount > 1 && ((min($values) > 0) || (max($values) < 0))) { - return Functions::NAN(); + throw new Exception(Functions::NAN()); } - $date0 = DateTimeExcel\Helpers::getDateValue($dates[0]); - if (is_string($date0)) { - return Functions::VALUE(); - } - - return ''; } } diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php new file mode 100644 index 00000000..9f7cf755 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/Financial/PmtTest.php @@ -0,0 +1,34 @@ +