diff --git a/docs/references/function-list-by-category.md b/docs/references/function-list-by-category.md index e6a702028..25dde055f 100644 --- a/docs/references/function-list-by-category.md +++ b/docs/references/function-list-by-category.md @@ -251,6 +251,7 @@ FORMULATEXT | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Formu GETPIVOTDATA | **Not yet Implemented** GROUPBY | **Not yet Implemented** HLOOKUP | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\HLookup::lookup +HSTACK | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Hstack::hstack HYPERLINK | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Hyperlink::set INDEX | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix::index INDIRECT | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Indirect::INDIRECT @@ -266,6 +267,7 @@ TAKE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Choos TRANSPOSE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix::transpose UNIQUE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Unique::unique VLOOKUP | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\VLookup::lookup +VSTACK | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Vstack::vstack XLOOKUP | **Not yet Implemented** XMATCH | **Not yet Implemented** @@ -308,7 +310,6 @@ FLOOR | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Floor: FLOOR.MATH | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Floor::math FLOOR.PRECISE | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Floor::precise GCD | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Gcd::evaluate -HSTACK | **Not yet Implemented** INT | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\IntClass::evaluate ISO.CEILING | **Not yet Implemented** LCM | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Lcm::evaluate @@ -360,7 +361,6 @@ TANH | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\T TOCOL | **Not yet Implemented** TOROW | **Not yet Implemented** TRUNC | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trunc::evaluate -VSTACK | **Not yet Implemented** WRAPCOLS | **Not yet Implemented** WRAPROWS | **Not yet Implemented** diff --git a/docs/references/function-list-by-name-compact.md b/docs/references/function-list-by-name-compact.md index b5f81b059..48ee91be0 100644 --- a/docs/references/function-list-by-name-compact.md +++ b/docs/references/function-list-by-name-compact.md @@ -259,7 +259,7 @@ HEX2DEC | ENGINEERING | Engineering\ConvertHex::toDec HEX2OCT | ENGINEERING | Engineering\ConvertHex::toOctal HLOOKUP | LOOKUP_AND_REFERENCE | LookupRef\HLookup::lookup HOUR | DATE_AND_TIME | DateTimeExcel\TimeParts::hour -HSTACK | MATH_AND_TRIG | **Not yet Implemented** +HSTACK | LOOKUP_AND_REFERENCE | LookupRef\Hstack::hstack HYPERLINK | LOOKUP_AND_REFERENCE | LookupRef\Hyperlink::set HYPGEOM.DIST | STATISTICAL | **Not yet Implemented** HYPGEOMDIST | STATISTICAL | Statistical\Distributions\HyperGeometric::distribution @@ -619,7 +619,7 @@ VARP | STATISTICAL | Statistical\Variances::VARP VARPA | STATISTICAL | Statistical\Variances::VARPA VDB | FINANCIAL | **Not yet Implemented** VLOOKUP | LOOKUP_AND_REFERENCE | LookupRef\VLookup::lookup -VSTACK | MATH_AND_TRIG | **Not yet Implemented** +VSTACK | LOOKUP_AND_REFERENCE | LookupRef\Vstack::vstack ## W diff --git a/docs/references/function-list-by-name.md b/docs/references/function-list-by-name.md index c2ece937a..2e183f15e 100644 --- a/docs/references/function-list-by-name.md +++ b/docs/references/function-list-by-name.md @@ -255,7 +255,7 @@ HEX2DEC | CATEGORY_ENGINEERING | \PhpOffice\PhpSpread HEX2OCT | CATEGORY_ENGINEERING | \PhpOffice\PhpSpreadsheet\Calculation\Engineering\ConvertHex::toOctal HLOOKUP | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\HLookup::lookup HOUR | CATEGORY_DATE_AND_TIME | \PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\TimeParts::hour -HSTACK | CATEGORY_MATH_AND_TRIG | **Not yet Implemented** +HSTACK | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Hstack::hstack HYPERLINK | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Hyperlink::set HYPGEOM.DIST | CATEGORY_STATISTICAL | **Not yet Implemented** HYPGEOMDIST | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\HyperGeometric::distribution @@ -615,7 +615,7 @@ VARP | CATEGORY_STATISTICAL | \PhpOffice\PhpSpread VARPA | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Variances::VARPA VDB | CATEGORY_FINANCIAL | **Not yet Implemented** VLOOKUP | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\VLookup::lookup -VSTACK | CATEGORY_MATH_AND_TRIG | **Not yet Implemented** +VSTACK | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Vstack::vstack ## W diff --git a/src/PhpSpreadsheet/Calculation/FunctionArray.php b/src/PhpSpreadsheet/Calculation/FunctionArray.php index 0692d34a8..0813cee8b 100644 --- a/src/PhpSpreadsheet/Calculation/FunctionArray.php +++ b/src/PhpSpreadsheet/Calculation/FunctionArray.php @@ -1106,8 +1106,8 @@ class FunctionArray extends CalculationBase 'argumentCount' => '1', ], 'HSTACK' => [ - 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Hstack::class, 'hstack'], 'argumentCount' => '1+', ], 'HYPERLINK' => [ @@ -2569,8 +2569,8 @@ class FunctionArray extends CalculationBase 'argumentCount' => '3,4', ], 'VSTACK' => [ - 'category' => Category::CATEGORY_MATH_AND_TRIG, - 'functionCall' => [Functions::class, 'DUMMY'], + 'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE, + 'functionCall' => [LookupRef\Vstack::class, 'vstack'], 'argumentCount' => '1+', ], 'WEBSERVICE' => [ diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php b/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php new file mode 100644 index 000000000..9be77e5c5 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Hstack.php @@ -0,0 +1,48 @@ + $rows) { + $matrix[] = array_pad([], $columns, ExcelError::NA()); + ++$rows; + } + } + + $transpose = array_map(null, ...$inputData); //* @phpstan-ignore-line + $returnMatrix = []; + foreach ($transpose as $array) { + $returnMatrix[] = Functions::flattenArray($array); + } + + return $returnMatrix; + } +} diff --git a/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php b/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php new file mode 100644 index 000000000..919c262a5 --- /dev/null +++ b/src/PhpSpreadsheet/Calculation/LookupRef/Vstack.php @@ -0,0 +1,42 @@ +setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheet = $spreadsheet->getActiveSheet(); + $b4tof14 = [ + ['A', null, 'B', null, 'C'], + [12, null, 7, null, 12], + [9, null, 13, null, 10], + [10, null, 5, null, 11], + [11, null, 13, null, 6], + [8, null, 12, null, 7], + [12, null, 11, null, 15], + [9, null, 10, null, 6], + [10, null, 15, null, 5], + [11, null, 9, null, 14], + [6, null, 13, null, 11], + ]; + $sheet->fromArray($b4tof14, null, 'B4', true); + $sheet->setCellValue('H4', '=HSTACK(B4:B14,D4:D14,F4:F14)'); + $expected = [ + ['A', 'B', 'C'], + [12, 7, 12], + [9, 13, 10], + [10, 5, 11], + [11, 13, 6], + [8, 12, 7], + [12, 11, 15], + [9, 10, 6], + [10, 15, 5], + [11, 9, 14], + [6, 13, 11], + ]; + self::assertSame($expected, $sheet->getCell('H4')->getCalculatedValue()); + + $sheet->setCellValue('K4', '=HSTACK(B4:B14,D4:D12,F4:F14)'); + $expected = [ + ['A', 'B', 'C'], + [12, 7, 12], + [9, 13, 10], + [10, 5, 11], + [11, 13, 6], + [8, 12, 7], + [12, 11, 15], + [9, 10, 6], + [10, 15, 5], + [11, '#N/A', 14], + [6, '#N/A', 11], + ]; + self::assertSame($expected, $sheet->getCell('K4')->getCalculatedValue(), 'one short column'); + + $sheet->setCellValue('R4', '=IFERROR(HSTACK(B4:B14,D4:D12,F4:F14),"")'); + $expected = [ + ['A', 'B', 'C'], + [12, 7, 12], + [9, 13, 10], + [10, 5, 11], + [11, 13, 6], + [8, 12, 7], + [12, 11, 15], + [9, 10, 6], + [10, 15, 5], + [11, '', 14], + [6, '', 11], + ]; + self::assertSame($expected, $sheet->getCell('R4')->getCalculatedValue(), 'one short column with null string instead of N/A'); + + $sheet->setCellValue('N4', '=HSTACK(B5,H6:J6)'); + $expected = [ + [12, 9, 13, 10], + ]; + self::assertSame($expected, $sheet->getCell('N4')->getCalculatedValue(), 'one single-cell arg'); + + $spreadsheet->disconnectWorksheets(); + } +} diff --git a/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VStackTest.php b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VStackTest.php new file mode 100644 index 000000000..2037bb70b --- /dev/null +++ b/tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/VStackTest.php @@ -0,0 +1,175 @@ +setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B2', 'Array1'); + $sheet->setCellValue('B3', 'Red'); + $sheet->setCellValue('B4', 'Blue'); + $sheet->setCellValue('B5', 'Green'); + $sheet->setCellValue('B7', 'Array2'); + $sheet->setCellValue('B8', 'Green'); + $sheet->setCellValue('B9', 'Red'); + $sheet->setCellValue('D2', 'Result'); + $sheet->setCellValue('D3', '=VSTACK(B3:B5,B8:B9)'); + $expected = [ + ['Red'], + ['Blue'], + ['Green'], + ['Green'], + ['Red'], + ]; + self::assertSame($expected, $sheet->getCell('D3')->getCalculatedValue()); + + $sheet->setCellValue('F2', '=VSTACK(B2,B8:B9)'); + $expected = [ + ['Array1'], + ['Green'], + ['Red'], + ]; + self::assertSame($expected, $sheet->getCell('F2')->getCalculatedValue(), 'one single-cell argument'); + + $spreadsheet->disconnectWorksheets(); + } + + public static function testVstack2(): void + { + $spreadsheet = new Spreadsheet(); + Calculation::getInstance($spreadsheet) + ->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheet = $spreadsheet->getActiveSheet(); + $sheet->setCellValue('B3', 'Red'); + $sheet->setCellValue('B4', 'Blue'); + $sheet->setCellValue('B5', 'Green'); + $sheet->setCellValue('B6', 'Blue'); + $sheet->setCellValue('B7', 'Red'); + $sheet->setCellValue('C3', 12); + $sheet->setCellValue('C4', 9); + $sheet->setCellValue('C5', 10); + $sheet->setCellValue('C5', 10); + $sheet->setCellValue('C6', 11); + $sheet->setCellValue('C7', 8); + $sheet->setCellValue('E3', '=VSTACK({"Color","Qty"},B3:C7)'); + $expected = [ + ['Color', 'Qty'], + ['Red', 12], + ['Blue', 9], + ['Green', 10], + ['Blue', 11], + ['Red', 8], + ]; + self::assertSame($expected, $sheet->getCell('E3')->getCalculatedValue()); + $sheet->setCellValue('A1', 'Purple'); + $sheet->setCellValue('A2', 'Orange'); + $sheet->setCellValue('H3', '=VSTACK({"Color","Qty"},A1:A2,B3:C7)'); + $expected = [ + ['Color', 'Qty'], + ['Purple', '#N/A'], + ['Orange', '#N/A'], + ['Red', 12], + ['Blue', 9], + ['Green', 10], + ['Blue', 11], + ['Red', 8], + ]; + self::assertSame($expected, $sheet->getCell('H3')->getCalculatedValue()); + $spreadsheet->disconnectWorksheets(); + } + + /** + * Excel has a problem here. + * If it reads VSTACK(Table1, Table2) without square + * brackets after Table1/2, it calculates the result as #NAME?. + * You can then just edit the formula without making + * any changes, and it will calculate it correctly, + * but it will add in the brackets when saving. + * This seems pretty buggy to me. + * PhpSpreadsheet will handle the formula with or without + * the brackets, but you should specify them to avoid + * problems with Excel. + */ + public static function testTables(): void + { + $spreadsheet = new Spreadsheet(); + Calculation::getInstance($spreadsheet) + ->setInstanceArrayReturnType( + Calculation::RETURN_ARRAY_AS_ARRAY + ); + $sheet = $spreadsheet->getActiveSheet(); + + $data = [ + ['Date', 'Color', 'Qty'], + [Date::stringToExcel('2021-04-03'), 'Red', 12], + [Date::stringToExcel('2021-04-07'), 'Blue', 9], + [Date::stringToExcel('2021-04-11'), 'Green', 10], + [Date::stringToExcel('2021-04-15'), 'Blue', 11], + [Date::stringToExcel('2021-04-20'), 'Red', 8], + ]; + $sheet->fromArray($data, null, 'B4', true); + $table = new Table('B4:D9', 'Table1'); + $sheet->addTable($table); + + $data = [ + ['Date', 'Color', 'Qty'], + [Date::stringToExcel('2021-05-05'), 'Red', 12], + [Date::stringToExcel('2021-05-12'), 'Blue', 9], + [Date::stringToExcel('2021-05-18'), 'Green', 10], + [Date::stringToExcel('2021-05-21'), 'Blue', 11], + [Date::stringToExcel('2021-05-28'), 'Green', 6], + ]; + $sheet->fromArray($data, null, 'B11', true); + $table = new Table('B11:D16', 'Table2'); + $sheet->addTable($table); + + $sheet->setCellValue('G4', 'Date'); + $sheet->setCellValue('H4', 'Color'); + $sheet->setCellValue('I4', 'Qty'); + $sheet->setCellValue('G5', '=VSTACK(Table1[],Table2[])'); + $sheet->getCell('G5')->getCalculatedValue(); + + $sheet->getStyle('B4:B16') + ->getNumberFormat() + ->setFormatCode('d-mmm'); + $sheet->getStyle('G5:G14') + ->getNumberFormat() + ->setFormatCode('d-mmm'); + $expected = [ + ['3-Apr', 'Red', '12'], + ['7-Apr', 'Blue', '9'], + ['11-Apr', 'Green', '10'], + ['15-Apr', 'Blue', '11'], + ['20-Apr', 'Red', '8'], + ['5-May', 'Red', '12'], + ['12-May', 'Blue', '9'], + ['18-May', 'Green', '10'], + ['21-May', 'Blue', '11'], + ['28-May', 'Green', '6'], + ]; + $actual = $sheet->rangeToArray('G5:I14', null, true, true); + self::assertSame($expected, $actual); + + $spreadsheet->disconnectWorksheets(); + } +}