From 60023e48f2ade64fe4a52036a09be5c700737d42 Mon Sep 17 00:00:00 2001 From: MarkBaker Date: Wed, 3 Mar 2021 18:49:46 +0100 Subject: [PATCH 01/47] 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 02/47] 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 ed62526acad6dc0d8da3a5f14e27d207379ce1e3 Mon Sep 17 00:00:00 2001 From: Mark Baker Date: Sun, 14 Mar 2021 19:58:10 +0100 Subject: [PATCH 03/47] 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 04/47] 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 05/47] 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 06/47] 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 07/47] 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 08/47] 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 09/47] 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 10/47] 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 11/47] 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 12/47] 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 13/47] 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 14/47] 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 15/47] 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 16/47] 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 17/47] 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 18/47] 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 19/47] 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 20/47] 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 21/47] 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 22/47] 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 23/47] 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 24/47] 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 25/47] 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 26/47] 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 27/47] 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 28/47] 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 29/47] 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 30/47] 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 31/47] 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 32/47] 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 33/47] 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 34/47] 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 35/47] 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 36/47] 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 37/47] 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 38/47] 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 39/47] 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 40/47] 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 41/47] 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 42/47] 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 43/47] 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 44/47] 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 45/47] 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 46/47] 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 47/47] 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. *