Skip to content

Commit 4d867c9

Browse files
author
MarkBaker
committed
Initial work enabling Excel function implementations for handling arrays as aguments when used in "array formulae".
So far: - handling for single argument functions - for functions where only one of the arguments is an array (a matrix or a row/column vector) - for when there are two array arguments, and one is a row vector, the other a column vector - for when there are either 2 row vectors, or 2 column vectors Will work ok, as long as there are no more than two array arguments; still need to identify the logic to apply when there are more than two arrays; or there are two that aren't an already supported row vector/column vector pairing (ie two matrices)
1 parent 394b504 commit 4d867c9

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+1837
-83
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Calculation;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Engine\ArrayArgumentHelper;
6+
7+
trait ArrayEnabled
8+
{
9+
/**
10+
* @var ArrayArgumentHelper
11+
*/
12+
private static $arrayArgumentHelper;
13+
14+
private static function initialiseHelper(array $arguments): void
15+
{
16+
if (self::$arrayArgumentHelper === null) {
17+
self::$arrayArgumentHelper = new ArrayArgumentHelper();
18+
}
19+
self::$arrayArgumentHelper->initialise($arguments);
20+
}
21+
22+
protected static function evaluateSingleArgumentArray(callable $method, array $values): array
23+
{
24+
$result = [];
25+
foreach ($values as $value) {
26+
$result[] = $method($value);
27+
}
28+
29+
return $result;
30+
}
31+
32+
/**
33+
* @param mixed ...$arguments
34+
*/
35+
protected static function evaluateArrayArguments(callable $method, ...$arguments): array
36+
{
37+
self::initialiseHelper($arguments);
38+
$arguments = self::$arrayArgumentHelper->arguments();
39+
40+
if (self::$arrayArgumentHelper->hasArrayArgument() === false) {
41+
return [$method(...$arguments)];
42+
}
43+
44+
if (self::$arrayArgumentHelper->arrayArgumentCount() === 1) {
45+
$nthArgument = self::$arrayArgumentHelper->getFirstArrayArgumentNumber();
46+
47+
return self::evaluateNthArgumentAsArray($method, $nthArgument, ...$arguments);
48+
}
49+
50+
$singleRowVectorIndex = self::$arrayArgumentHelper->getSingleRowVector();
51+
$singleColumnVectorIndex = self::$arrayArgumentHelper->getSingleColumnVector();
52+
if ($singleRowVectorIndex !== null && $singleColumnVectorIndex !== null) {
53+
// Basic logic for a single row vector and a single column vector
54+
return self::evaluateVectorPair($method, $singleRowVectorIndex, $singleColumnVectorIndex, ...$arguments);
55+
}
56+
57+
$rowVectorIndexes = self::$arrayArgumentHelper->getRowVectors();
58+
$columnVectorIndexes = self::$arrayArgumentHelper->getColumnVectors();
59+
60+
// Logic for a two row vectors or two column vectors
61+
if (count($rowVectorIndexes) === 2 && count($columnVectorIndexes) === 0) {
62+
return self::evaluateRowVectorPair($method, $rowVectorIndexes, ...$arguments);
63+
} elseif (count($rowVectorIndexes) === 0 && count($columnVectorIndexes) === 2) {
64+
return self::evaluateColumnVectorPair($method, $columnVectorIndexes, ...$arguments);
65+
}
66+
67+
// If we have multiple arrays, and they don't match a row vector/column vector pattern,
68+
// or two row vectors and two column vectors,
69+
// then we drop through to an error return for the moment
70+
// Still need to work out the logic for multiple matrices as array arguments,
71+
// or when we have more than two arrays
72+
return ['#VALUE!'];
73+
}
74+
75+
/**
76+
* @param mixed ...$arguments
77+
*/
78+
private static function evaluateRowVectorPair(callable $method, array $vectorIndexes, ...$arguments): array
79+
{
80+
$vector2 = array_pop($vectorIndexes);
81+
$vectorValues2 = Functions::flattenArray($arguments[$vector2]);
82+
$vector1 = array_pop($vectorIndexes);
83+
$vectorValues1 = Functions::flattenArray($arguments[$vector1]);
84+
85+
$result = [];
86+
foreach ($vectorValues1 as $index => $value1) {
87+
$value2 = $vectorValues2[$index];
88+
$arguments[$vector1] = $value1;
89+
$arguments[$vector2] = $value2;
90+
91+
$result[] = $method(...$arguments);
92+
}
93+
94+
return [$result];
95+
}
96+
97+
/**
98+
* @param mixed ...$arguments
99+
*/
100+
private static function evaluateColumnVectorPair(callable $method, array $vectorIndexes, ...$arguments): array
101+
{
102+
$vector2 = array_pop($vectorIndexes);
103+
$vectorValues2 = Functions::flattenArray($arguments[$vector2]);
104+
$vector1 = array_pop($vectorIndexes);
105+
$vectorValues1 = Functions::flattenArray($arguments[$vector1]);
106+
107+
$result = [];
108+
foreach ($vectorValues1 as $index => $value1) {
109+
$value2 = $vectorValues2[$index];
110+
$arguments[$vector1] = $value1;
111+
$arguments[$vector2] = $value2;
112+
113+
$result[] = [$method(...$arguments)];
114+
}
115+
116+
return $result;
117+
}
118+
119+
/**
120+
* @param mixed ...$arguments
121+
*/
122+
private static function evaluateVectorPair(callable $method, int $rowIndex, int $columnIndex, ...$arguments): array
123+
{
124+
$rowVector = Functions::flattenArray($arguments[$rowIndex]);
125+
$columnVector = Functions::flattenArray($arguments[$columnIndex]);
126+
127+
$result = [];
128+
foreach ($columnVector as $column) {
129+
$rowResults = [];
130+
foreach ($rowVector as $row) {
131+
$arguments[$rowIndex] = $row;
132+
$arguments[$columnIndex] = $column;
133+
134+
$rowResults[] = $method(...$arguments);
135+
}
136+
$result[] = $rowResults;
137+
}
138+
139+
return $result;
140+
}
141+
142+
/**
143+
* Note, offset is from 1 (for the first argument) rather than from 0.
144+
*
145+
* @param mixed ...$arguments
146+
*/
147+
private static function evaluateNthArgumentAsArray(callable $method, int $nthArgument, ...$arguments): array
148+
{
149+
$values = array_slice($arguments, $nthArgument - 1, 1);
150+
/** @var array $values */
151+
$values = array_pop($values);
152+
153+
$result = [];
154+
foreach ($values as $value) {
155+
$arguments[$nthArgument - 1] = $value;
156+
$result[] = $method(...$arguments);
157+
}
158+
159+
return $result;
160+
}
161+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheet\Calculation\Engine;
4+
5+
class ArrayArgumentHelper
6+
{
7+
/**
8+
* @var array
9+
*/
10+
protected $arguments;
11+
12+
/**
13+
* @var int
14+
*/
15+
protected $argumentCount;
16+
17+
/**
18+
* @var array
19+
*/
20+
protected $rows;
21+
22+
/**
23+
* @var array
24+
*/
25+
protected $columns;
26+
27+
public function initialise(array $arguments): void
28+
{
29+
$this->rows = $this->rows($arguments);
30+
$this->columns = $this->columns($arguments);
31+
32+
$this->argumentCount = count($arguments);
33+
$this->arguments = $this->flattenSingleCellArrays($arguments, $this->rows, $this->columns);
34+
35+
$this->rows = $this->rows($arguments);
36+
$this->columns = $this->columns($arguments);
37+
}
38+
39+
public function arguments(): array
40+
{
41+
return $this->arguments;
42+
}
43+
44+
public function hasArrayArgument(): bool
45+
{
46+
return $this->arrayArgumentCount() > 0;
47+
}
48+
49+
public function arrayArgumentCount(): int
50+
{
51+
$rowArrays = $this->filterArray($this->rows);
52+
$columnArrays = $this->filterArray($this->columns);
53+
54+
$count = 0;
55+
for ($index = 0; $index < $this->argumentCount; ++$index) {
56+
if (isset($rowArrays[$index]) || isset($columnArrays[$index])) {
57+
++$count;
58+
}
59+
}
60+
61+
return $count;
62+
}
63+
64+
public function getFirstArrayArgumentNumber(): int
65+
{
66+
$rowArrays = $this->filterArray($this->rows);
67+
$columnArrays = $this->filterArray($this->columns);
68+
69+
for ($index = 0; $index < $this->argumentCount; ++$index) {
70+
if (isset($rowArrays[$index]) || isset($columnArrays[$index])) {
71+
return ++$index;
72+
}
73+
}
74+
75+
return 0;
76+
}
77+
78+
public function getRowVectors(): array
79+
{
80+
$rowVectors = [];
81+
for ($index = 0; $index < $this->argumentCount; ++$index) {
82+
if ($this->rows[$index] === 1 && $this->columns[$index] > 1) {
83+
$rowVectors[] = $index;
84+
}
85+
}
86+
87+
return $rowVectors;
88+
}
89+
90+
public function getSingleRowVector(): ?int
91+
{
92+
$rowVectors = $this->getRowVectors();
93+
94+
return count($rowVectors) === 1 ? array_pop($rowVectors) : null;
95+
}
96+
97+
public function getColumnVectors(): array
98+
{
99+
$columnVectors = [];
100+
for ($index = 0; $index < $this->argumentCount; ++$index) {
101+
if ($this->rows[$index] > 1 && $this->columns[$index] === 1) {
102+
$columnVectors[] = $index;
103+
}
104+
}
105+
106+
return $columnVectors;
107+
}
108+
109+
public function getSingleColumnVector(): ?int
110+
{
111+
$columnVectors = $this->getColumnVectors();
112+
113+
return count($columnVectors) === 1 ? array_pop($columnVectors) : null;
114+
}
115+
116+
private function rows(array $arguments): array
117+
{
118+
return array_map(
119+
function ($argument) {
120+
return is_countable($argument) ? count($argument) : 1;
121+
},
122+
$arguments
123+
);
124+
}
125+
126+
private function columns(array $arguments): array
127+
{
128+
return array_map(
129+
function ($argument) {
130+
return is_array($argument) && is_array($argument[array_keys($argument)[0]])
131+
? count($argument[array_keys($argument)[0]])
132+
: 1;
133+
},
134+
$arguments
135+
);
136+
}
137+
138+
private function flattenSingleCellArrays(array $arguments, array $rows, array $columns): array
139+
{
140+
foreach ($arguments as $index => $argument) {
141+
if ($rows[$index] === 1 && $columns[$index] === 1) {
142+
while (is_array($argument)) {
143+
$argument = array_pop($argument);
144+
}
145+
$arguments[$index] = $argument;
146+
}
147+
}
148+
149+
return $arguments;
150+
}
151+
152+
private function filterArray(array $array): array
153+
{
154+
return array_filter(
155+
$array,
156+
function ($value) {
157+
return $value > 1;
158+
}
159+
);
160+
}
161+
}

src/PhpSpreadsheet/Calculation/MathTrig/Absolute.php

+11-2
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,30 @@
22

33
namespace PhpOffice\PhpSpreadsheet\Calculation\MathTrig;
44

5+
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
56
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
67

78
class Absolute
89
{
10+
use ArrayEnabled;
11+
912
/**
1013
* ABS.
1114
*
1215
* Returns the result of builtin function abs after validating args.
1316
*
14-
* @param mixed $number Should be numeric
17+
* @param mixed $number Should be numeric, or can be an array of numbers
1518
*
16-
* @return float|int|string Rounded number
19+
* @return array|float|int|string rounded number
20+
* If an array of numbers is passed as the argument, then the returned result will also be an array
21+
* with the same dimensions
1722
*/
1823
public static function evaluate($number)
1924
{
25+
if (is_array($number)) {
26+
return self::evaluateSingleArgumentArray([self::class, __FUNCTION__], $number);
27+
}
28+
2029
try {
2130
$number = Helpers::validateNumericNullBool($number);
2231
} catch (Exception $e) {

0 commit comments

Comments
 (0)