Skip to content

Commit f3c3aba

Browse files
committed
TOCOL and TOROW
TOCOL and TOROW were introduced to Excel in 2024, and will now be supported by PhpSpreadsheet. The documentation says that, under certain circumstances, "blanks" will be ignored. This seems demonstrably wrong. In the right circumstance, Excel will ignore nulls, not blanks. Further, when it decides to not ignore the nulls, it changes them to 0, which also seems insufficiently documented. PhpSpreadsheet will behave as Excel does. I discovered some minor problems and some missing test conditions for the TRANSPOSE function while testing these. Those are now fixed.
1 parent c00c2df commit f3c3aba

File tree

11 files changed

+283
-10
lines changed

11 files changed

+283
-10
lines changed

docs/references/function-list-by-category.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,8 @@ RTD | **Not yet Implemented**
263263
SORT | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Sort::sort
264264
SORTBY | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Sort::sortBy
265265
TAKE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\ChooseRowsEtc::take
266+
TOCOL | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\TorowTocol::tocol
267+
TOROW | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\TorowTocol::torow
266268
TRANSPOSE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix::transpose
267269
UNIQUE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Unique::unique
268270
VLOOKUP | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\VLookup::lookup
@@ -357,8 +359,6 @@ SUMX2PY2 | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\SumSqu
357359
SUMXMY2 | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\SumSquares::sumXMinusYSquared
358360
TAN | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Tangent::tan
359361
TANH | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trig\Tangent::tanh
360-
TOCOL | **Not yet Implemented**
361-
TOROW | **Not yet Implemented**
362362
TRUNC | \PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Trunc::evaluate
363363
VSTACK | **Not yet Implemented**
364364
WRAPCOLS | **Not yet Implemented**

docs/references/function-list-by-name-compact.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,9 +583,9 @@ THAIYEAR | DATE_AND_TIME | **Not yet Implemented**
583583
TIME | DATE_AND_TIME | DateTimeExcel\Time::fromHMS
584584
TIMEVALUE | DATE_AND_TIME | DateTimeExcel\TimeValue::fromString
585585
TINV | STATISTICAL | Statistical\Distributions\StudentT::inverse
586-
TOCOL | MATH_AND_TRIG | **Not yet Implemented**
586+
TOCOL | LOOKUP_AND_REFERENCE | LookupRef\TorowTocol::tocol
587587
TODAY | DATE_AND_TIME | DateTimeExcel\Current::today
588-
TOROW | MATH_AND_TRIG | **Not yet Implemented**
588+
TOROW | LOOKUP_AND_REFERENCE | LookupRef\TorowTocol::torow
589589
TRANSPOSE | LOOKUP_AND_REFERENCE | LookupRef\Matrix::transpose
590590
TREND | STATISTICAL | Statistical\Trends::TREND
591591
TRIM | TEXT_AND_DATA | TextData\Trim::spaces

docs/references/function-list-by-name.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -579,9 +579,9 @@ THAIYEAR | CATEGORY_DATE_AND_TIME | **Not yet Implemente
579579
TIME | CATEGORY_DATE_AND_TIME | \PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Time::fromHMS
580580
TIMEVALUE | CATEGORY_DATE_AND_TIME | \PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\TimeValue::fromString
581581
TINV | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions\StudentT::inverse
582-
TOCOL | CATEGORY_MATH_AND_TRIG | **Not yet Implemented**
582+
TOCOL | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\TorowTocol::tocol
583583
TODAY | CATEGORY_DATE_AND_TIME | \PhpOffice\PhpSpreadsheet\Calculation\DateTimeExcel\Current::today
584-
TOROW | CATEGORY_MATH_AND_TRIG | **Not yet Implemented**
584+
TOROW | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\TorowTocol::torow
585585
TRANSPOSE | CATEGORY_LOOKUP_AND_REFERENCE | \PhpOffice\PhpSpreadsheet\Calculation\LookupRef\Matrix::transpose
586586
TREND | CATEGORY_STATISTICAL | \PhpOffice\PhpSpreadsheet\Calculation\Statistical\Trends::TREND
587587
TRIM | CATEGORY_TEXT_AND_DATA | \PhpOffice\PhpSpreadsheet\Calculation\TextData\Trim::spaces

src/PhpSpreadsheet/Calculation/FunctionArray.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2439,13 +2439,13 @@ class FunctionArray extends CalculationBase
24392439
'argumentCount' => '0',
24402440
],
24412441
'TOCOL' => [
2442-
'category' => Category::CATEGORY_MATH_AND_TRIG,
2443-
'functionCall' => [Functions::class, 'DUMMY'],
2442+
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2443+
'functionCall' => [LookupRef\TorowTocol::class, 'tocol'],
24442444
'argumentCount' => '1-3',
24452445
],
24462446
'TOROW' => [
2447-
'category' => Category::CATEGORY_MATH_AND_TRIG,
2448-
'functionCall' => [Functions::class, 'DUMMY'],
2447+
'category' => Category::CATEGORY_LOOKUP_AND_REFERENCE,
2448+
'functionCall' => [LookupRef\TorowTocol::class, 'torow'],
24492449
'argumentCount' => '1-3',
24502450
],
24512451
'TRANSPOSE' => [

src/PhpSpreadsheet/Calculation/LookupRef/Matrix.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ public static function transpose($matrixData): array
4444
if (!is_array($matrixData)) {
4545
$matrixData = [[$matrixData]];
4646
}
47+
if (!is_array(end($matrixData))) {
48+
$matrixData = [$matrixData];
49+
}
4750

4851
$column = 0;
4952
/** @var mixed[][] $matrixData */
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
6+
use PhpOffice\PhpSpreadsheet\Calculation\Information\ErrorValue;
7+
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
8+
9+
class TorowTocol
10+
{
11+
/**
12+
* Excel function TOCOL.
13+
*
14+
* @return mixed[]|string
15+
*/
16+
public static function tocol(mixed $array, mixed $ignore = 0, mixed $byColumn = false): array|string
17+
{
18+
$result = self::torow($array, $ignore, $byColumn);
19+
if (is_array($result)) {
20+
return array_map((fn ($x) => [$x]), $result);
21+
}
22+
23+
return $result;
24+
}
25+
26+
/**
27+
* Excel function TOROW.
28+
*
29+
* @return mixed[]|string
30+
*/
31+
public static function torow(mixed $array, mixed $ignore = 0, mixed $byColumn = false): array|string
32+
{
33+
if (!is_numeric($ignore)) {
34+
return ExcelError::VALUE();
35+
}
36+
$ignore = (int) $ignore;
37+
if ($ignore < 0 || $ignore > 3) {
38+
return ExcelError::VALUE();
39+
}
40+
if (is_int($byColumn) || is_float($byColumn)) {
41+
$byColumn = (bool) $byColumn;
42+
}
43+
if (!is_bool($byColumn)) {
44+
return ExcelError::VALUE();
45+
}
46+
if (!is_array($array)) {
47+
$array = [$array];
48+
}
49+
if ($byColumn) {
50+
$temp = [];
51+
foreach ($array as $row) {
52+
if (!is_array($row)) {
53+
$row = [$row];
54+
}
55+
$temp[] = Functions::flattenArray($row);
56+
}
57+
$array = ChooseRowsEtc::transpose($temp);
58+
} else {
59+
$array = Functions::flattenArray($array);
60+
}
61+
62+
return self::byRow($array, $ignore);
63+
}
64+
65+
/**
66+
* @param mixed[] $array
67+
*
68+
* @return mixed[]
69+
*/
70+
private static function byRow(array $array, int $ignore): array
71+
{
72+
$returnMatrix = [];
73+
foreach ($array as $row) {
74+
if (!is_array($row)) {
75+
$row = [$row];
76+
}
77+
foreach ($row as $cell) {
78+
if ($cell === null) {
79+
if ($ignore === 1 || $ignore === 3) {
80+
continue;
81+
}
82+
$cell = 0;
83+
} elseif (ErrorValue::isError($cell)) {
84+
if ($ignore === 2 || $ignore === 3) {
85+
continue;
86+
}
87+
}
88+
$returnMatrix[] = $cell;
89+
}
90+
}
91+
92+
return $returnMatrix;
93+
}
94+
}

tests/PhpSpreadsheetTests/Calculation/Functions/LookupRef/AllSetupTeardown.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,4 +95,13 @@ protected function setArrayAsValue(): void
9595
Calculation::RETURN_ARRAY_AS_VALUE
9696
);
9797
}
98+
99+
protected function setArrayAsArray(): void
100+
{
101+
$spreadsheet = $this->getSpreadsheet();
102+
$calculation = Calculation::getInstance($spreadsheet);
103+
$calculation->setInstanceArrayReturnType(
104+
Calculation::RETURN_ARRAY_AS_ARRAY
105+
);
106+
}
98107
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
6+
7+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
10+
class TocolTest extends AllSetupTeardown
11+
{
12+
#[DataProvider('providerTocol')]
13+
public function testTorow(mixed $expectedResult, mixed $ignore = 'omitted', mixed $byColumn = 'omitted'): void
14+
{
15+
$this->mightHaveException($expectedResult);
16+
$sheet = $this->getSheet();
17+
$this->setArrayAsArray();
18+
if (is_string($ignore) && $ignore !== 'omitted') {
19+
$ignore = '"' . $ignore . '"';
20+
}
21+
if (is_string($byColumn) && $byColumn !== 'omitted') {
22+
$byColumn = '"' . $byColumn . '"';
23+
}
24+
$ignore = StringHelper::convertToString($ignore);
25+
$byColumn = StringHelper::convertToString($byColumn, convertBool: true);
26+
if ($ignore === 'omitted') {
27+
$formula = '=TOCOL(A1:D3)';
28+
} elseif ($byColumn === 'omitted') {
29+
$formula = "=TOCOL(A1:D3,$ignore)";
30+
} else {
31+
$formula = "=TOCOL(A1:D3,$ignore,$byColumn)";
32+
}
33+
34+
$data = [
35+
['a-one', 'b-one', 'c-one', 'd-one'],
36+
[null, 'b-two', 'c-two', '=2/0'],
37+
[' ', 'b-three', 'c-three', 'd-three'],
38+
];
39+
$sheet->fromArray($data, null, 'A1', true);
40+
$sheet->setCellValue('A5', $formula);
41+
$result = $sheet->getCell('A5')->getCalculatedValue();
42+
self::assertSame($expectedResult, $result);
43+
}
44+
45+
public static function providerTocol(): array
46+
{
47+
return [
48+
'defaults' => [[['a-one'], ['b-one'], ['c-one'], ['d-one'], [0], ['b-two'], ['c-two'], ['#DIV/0!'], [' '], ['b-three'], ['c-three'], ['d-three']]],
49+
'ignore=0' => [[['a-one'], ['b-one'], ['c-one'], ['d-one'], [0], ['b-two'], ['c-two'], ['#DIV/0!'], [' '], ['b-three'], ['c-three'], ['d-three']], 0],
50+
'ignore=1 supplied as 1.1' => [[['a-one'], ['b-one'], ['c-one'], ['d-one'], ['b-two'], ['c-two'], ['#DIV/0!'], [' '], ['b-three'], ['c-three'], ['d-three']], 1.1],
51+
'ignore=2' => [[['a-one'], ['b-one'], ['c-one'], ['d-one'], [0], ['b-two'], ['c-two'], [' '], ['b-three'], ['c-three'], ['d-three']], 2],
52+
'ignore=3' => [[['a-one'], ['b-one'], ['c-one'], ['d-one'], ['b-two'], ['c-two'], [' '], ['b-three'], ['c-three'], ['d-three']], 3],
53+
'ignore=4 invalid' => ['#VALUE!', 4],
54+
'ignore=string invalid' => ['#VALUE!', 'x'],
55+
'by column' => [[['a-one'], [0], [' '], ['b-one'], ['b-two'], ['b-three'], ['c-one'], ['c-two'], ['c-three'], ['d-one'], ['#DIV/0!'], ['d-three']], 0, true],
56+
'by column using float rather than bool, ignore=2' => [[['a-one'], [0], [' '], ['b-one'], ['b-two'], ['b-three'], ['c-one'], ['c-two'], ['c-three'], ['d-one'], ['d-three']], 2, 29.7],
57+
];
58+
}
59+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
6+
7+
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
8+
use PHPUnit\Framework\Attributes\DataProvider;
9+
10+
class TorowTest extends AllSetupTeardown
11+
{
12+
#[DataProvider('providerTorow')]
13+
public function testTorow(mixed $expectedResult, mixed $ignore = 'omitted', mixed $byColumn = 'omitted'): void
14+
{
15+
$this->mightHaveException($expectedResult);
16+
$sheet = $this->getSheet();
17+
$this->setArrayAsArray();
18+
if (is_string($ignore) && $ignore !== 'omitted') {
19+
$ignore = '"' . $ignore . '"';
20+
}
21+
if (is_string($byColumn) && $byColumn !== 'omitted') {
22+
$byColumn = '"' . $byColumn . '"';
23+
}
24+
$ignore = StringHelper::convertToString($ignore);
25+
$byColumn = StringHelper::convertToString($byColumn, convertBool: true);
26+
if ($ignore === 'omitted') {
27+
$formula = '=TOROW(A1:D3)';
28+
} elseif ($byColumn === 'omitted') {
29+
$formula = "=TOROW(A1:D3,$ignore)";
30+
} else {
31+
$formula = "=TOROW(A1:D3,$ignore,$byColumn)";
32+
}
33+
34+
$data = [
35+
['a-one', 'b-one', 'c-one', 'd-one'],
36+
[null, 'b-two', 'c-two', '=2/0'],
37+
[' ', 'b-three', 'c-three', 'd-three'],
38+
];
39+
$sheet->fromArray($data, null, 'A1', true);
40+
$sheet->setCellValue('A5', $formula);
41+
$result = $sheet->getCell('A5')->getCalculatedValue();
42+
self::assertSame($expectedResult, $result);
43+
}
44+
45+
public static function providerTorow(): array
46+
{
47+
return [
48+
'defaults' => [['a-one', 'b-one', 'c-one', 'd-one', 0, 'b-two', 'c-two', '#DIV/0!', ' ', 'b-three', 'c-three', 'd-three']],
49+
'ignore=0' => [['a-one', 'b-one', 'c-one', 'd-one', 0, 'b-two', 'c-two', '#DIV/0!', ' ', 'b-three', 'c-three', 'd-three'], 0],
50+
'ignore=1 supplied as 1.1' => [['a-one', 'b-one', 'c-one', 'd-one', 'b-two', 'c-two', '#DIV/0!', ' ', 'b-three', 'c-three', 'd-three'], 1.1],
51+
'ignore=2' => [['a-one', 'b-one', 'c-one', 'd-one', 0, 'b-two', 'c-two', ' ', 'b-three', 'c-three', 'd-three'], 2],
52+
'ignore=3' => [['a-one', 'b-one', 'c-one', 'd-one', 'b-two', 'c-two', ' ', 'b-three', 'c-three', 'd-three'], 3],
53+
'ignore=-1 invalid' => ['#VALUE!', -1],
54+
'ignore=string invalid' => ['#VALUE!', 'x'],
55+
'by column' => [['a-one', 0, ' ', 'b-one', 'b-two', 'b-three', 'c-one', 'c-two', 'c-three', 'd-one', '#DIV/0!', 'd-three'], 0, true],
56+
'by column using int rather than bool, ignore=1' => [['a-one', ' ', 'b-one', 'b-two', 'b-three', 'c-one', 'c-two', 'c-three', 'd-one', '#DIV/0!', 'd-three'], 1, -15],
57+
];
58+
}
59+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Calculation\Functions\LookupRef;
6+
7+
use PHPUnit\Framework\Attributes\DataProvider;
8+
9+
class TransposeOnSpreadsheetTest extends AllSetupTeardown
10+
{
11+
#[DataProvider('providerTRANSPOSE')]
12+
public function testTRANSPOSE(mixed $expectedResult, mixed $matrix): void
13+
{
14+
$sheet = $this->getSheet();
15+
$this->setArrayAsArray();
16+
if (!is_array($matrix)) {
17+
$matrix = [$matrix];
18+
}
19+
$sheet->fromArray($matrix, null, 'A1', true);
20+
$highColumn = $sheet->getHighestDataColumn();
21+
$highRow = $sheet->getHighestDataRow();
22+
$newHighColumn = $highColumn;
23+
++$newHighColumn;
24+
$sheet->getCell("{$newHighColumn}1")
25+
->setValue("=TRANSPOSE(A1:$highColumn$highRow)");
26+
self::assertSame($expectedResult, $sheet->getCell("{$newHighColumn}1")->getCalculatedValue());
27+
}
28+
29+
public static function providerTRANSPOSE(): array
30+
{
31+
return require 'tests/data/Calculation/LookupRef/TRANSPOSE.php';
32+
}
33+
}

tests/data/Calculation/LookupRef/TRANSPOSE.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,20 @@
3131
[[3.14]],
3232
3.14,
3333
],
34+
'single row' => [
35+
[[1], [2], [3], [4]],
36+
[[1, 2, 3, 4]],
37+
],
38+
'single row 1-D' => [
39+
[[1], [2], [3], [4]],
40+
[1, 2, 3, 4],
41+
],
42+
'single column' => [
43+
[[1, 2, 3, 4]],
44+
[[1], [2], [3], [4]],
45+
],
46+
'single cell' => [
47+
[[1]],
48+
[[1]],
49+
],
3450
];

0 commit comments

Comments
 (0)