Skip to content

Commit 4a28fd6

Browse files
authored
Merge pull request #2749 from PHPOffice/CalcEngine-Parser-Unit_Tests
Basic unit tests for formula parsing
2 parents 709e2ae + 483ef53 commit 4a28fd6

File tree

2 files changed

+143
-8
lines changed

2 files changed

+143
-8
lines changed

src/PhpSpreadsheet/Calculation/Calculation.php

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,8 @@ class Calculation
108108
'+' => true, '-' => true, '*' => true, '/' => true,
109109
'^' => true, '&' => true, '%' => false, '~' => false,
110110
'>' => true, '<' => true, '=' => true, '>=' => true,
111-
'<=' => true, '<>' => true, '|' => true, ':' => true,
111+
'<=' => true, '<>' => true, '' => true, '' => true,
112+
':' => true,
112113
];
113114

114115
/**
@@ -120,7 +121,7 @@ class Calculation
120121
'+' => true, '-' => true, '*' => true, '/' => true,
121122
'^' => true, '&' => true, '>' => true, '<' => true,
122123
'=' => true, '>=' => true, '<=' => true, '<>' => true,
123-
'|' => true, ':' => true,
124+
'' => true, '' => true, ':' => true,
124125
];
125126

126127
/**
@@ -3872,7 +3873,7 @@ private function convertMatrixReferences($formula)
38723873
'*' => 0, '/' => 0, // Multiplication and Division
38733874
'+' => 0, '-' => 0, // Addition and Subtraction
38743875
'&' => 0, // Concatenation
3875-
'|' => 0, ':' => 0, // Intersect and Range
3876+
'' => 0, '' => 0, ':' => 0, // Union, Intersect and Range
38763877
'>' => 0, '<' => 0, '=' => 0, '>=' => 0, '<=' => 0, '<>' => 0, // Comparison
38773878
];
38783879

@@ -3884,8 +3885,9 @@ private function convertMatrixReferences($formula)
38843885
// This list includes all valid operators, whether binary (including boolean) or unary (such as %)
38853886
// Array key is the operator, the value is its precedence
38863887
private static $operatorPrecedence = [
3887-
':' => 8, // Range
3888-
'|' => 7, // Intersect
3888+
':' => 9, // Range
3889+
'' => 8, // Intersect
3890+
'' => 7, // Union
38893891
'~' => 6, // Negation
38903892
'%' => 5, // Percentage
38913893
'^' => 4, // Exponentiation
@@ -3957,7 +3959,7 @@ private function internalParseFormula($formula, ?Cell $cell = null)
39573959
++$index;
39583960
} elseif ($opCharacter == '+' && !$expectingOperator) { // Positive (unary plus rather than binary operator plus) can be discarded?
39593961
++$index; // Drop the redundant plus symbol
3960-
} elseif ((($opCharacter == '~') || ($opCharacter == '|')) && (!$isOperandOrFunction)) { // We have to explicitly deny a tilde or pipe, because they are legal
3962+
} elseif ((($opCharacter == '~') || ($opCharacter == '') || ($opCharacter == '')) && (!$isOperandOrFunction)) { // We have to explicitly deny a tilde, union or intersect because they are legal
39613963
return $this->raiseFormulaError("Formula Error: Illegal character '~'"); // on the stack but not in the input expression
39623964
} elseif ((isset(self::$operators[$opCharacter]) || $isOperandOrFunction) && $expectingOperator) { // Are we putting an operator on the stack?
39633965
while (
@@ -4338,7 +4340,7 @@ private function internalParseFormula($formula, ?Cell $cell = null)
43384340
) {
43394341
$output[] = $stack->pop(); // Swap operands and higher precedence operators from the stack to the output
43404342
}
4341-
$stack->push('Binary Operator', '|'); // Put an Intersect Operator on the stack
4343+
$stack->push('Binary Operator', ''); // Put an Intersect Operator on the stack
43424344
$expectingOperator = false;
43434345
}
43444346
}
@@ -4638,7 +4640,7 @@ private function processTokenStack($tokens, $cellID = null, ?Cell $cell = null)
46384640
}
46394641

46404642
break;
4641-
case '|': // Intersect
4643+
case '': // Intersect
46424644
$rowIntersect = array_intersect_key($operand1, $operand2);
46434645
$cellIntersect = $oCol = $oRow = [];
46444646
foreach (array_keys($rowIntersect) as $row) {
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
4+
5+
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6+
use PHPUnit\Framework\TestCase;
7+
8+
class ParseFormulaTest extends TestCase
9+
{
10+
/**
11+
* @dataProvider providerBinaryOperations
12+
*/
13+
public function testParseOperations(array $expectedStack, string $formula): void
14+
{
15+
$parser = Calculation::getInstance();
16+
$stack = $parser->parseFormula($formula);
17+
self::assertSame($expectedStack, $stack);
18+
}
19+
20+
public function providerBinaryOperations(): array
21+
{
22+
return [
23+
'Unary negative with Value' => [
24+
[
25+
['type' => 'Value', 'value' => 3, 'reference' => null],
26+
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
27+
],
28+
'=-3',
29+
],
30+
'Unary negative percentage with Value' => [
31+
[
32+
['type' => 'Value', 'value' => 3, 'reference' => null],
33+
['type' => 'Unary Operator', 'value' => '%', 'reference' => null],
34+
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
35+
],
36+
'=-3%',
37+
],
38+
'Binary minus with Values' => [
39+
[
40+
['type' => 'Value', 'value' => 3, 'reference' => null],
41+
['type' => 'Value', 'value' => 4, 'reference' => null],
42+
['type' => 'Binary Operator', 'value' => '-', 'reference' => null],
43+
],
44+
'=3-4',
45+
],
46+
'Unary negative with Cell Reference' => [
47+
[
48+
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
49+
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
50+
],
51+
'=-A1',
52+
],
53+
'Unary negative with FQ Cell Reference' => [
54+
[
55+
['type' => 'Cell Reference', 'value' => "'Sheet 1'!A1", 'reference' => "'Sheet 1'!A1"],
56+
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
57+
],
58+
"=-'Sheet 1'!A1",
59+
],
60+
'Unary negative percentage with Cell Reference' => [
61+
[
62+
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
63+
['type' => 'Unary Operator', 'value' => '%', 'reference' => null],
64+
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
65+
],
66+
'=-A1%',
67+
],
68+
'Unary negative with Defined Name' => [
69+
[
70+
['type' => 'Defined Name', 'value' => 'DEFINED_NAME', 'reference' => 'DEFINED_NAME'],
71+
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
72+
],
73+
'=-DEFINED_NAME',
74+
],
75+
'Unary negative percentage with Defined Name' => [
76+
[
77+
['type' => 'Defined Name', 'value' => 'DEFINED_NAME', 'reference' => 'DEFINED_NAME'],
78+
['type' => 'Unary Operator', 'value' => '%', 'reference' => null],
79+
['type' => 'Unary Operator', 'value' => '~', 'reference' => null],
80+
],
81+
'=-DEFINED_NAME%',
82+
],
83+
'Cell Range' => [
84+
[
85+
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
86+
['type' => 'Cell Reference', 'value' => 'C3', 'reference' => 'C3'],
87+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
88+
],
89+
'=A1:C3',
90+
],
91+
'Cell Range Intersection' => [
92+
[
93+
['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
94+
['type' => 'Cell Reference', 'value' => 'C3', 'reference' => 'C3'],
95+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
96+
['type' => 'Cell Reference', 'value' => 'B2', 'reference' => 'B2'],
97+
['type' => 'Cell Reference', 'value' => 'D4', 'reference' => 'D4'],
98+
['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
99+
['type' => 'Binary Operator', 'value' => '', 'reference' => null],
100+
],
101+
'=A1:C3 B2:D4',
102+
],
103+
'Named Range Intersection' => [
104+
[
105+
['type' => 'Defined Name', 'value' => 'DEFINED_NAME_1', 'reference' => 'DEFINED_NAME_1'],
106+
['type' => 'Defined Name', 'value' => 'DEFINED_NAME_2', 'reference' => 'DEFINED_NAME_2'],
107+
['type' => 'Binary Operator', 'value' => '', 'reference' => null],
108+
],
109+
'=DEFINED_NAME_1 DEFINED_NAME_2',
110+
],
111+
// 'Cell Range Union' => [
112+
// [
113+
// ['type' => 'Cell Reference', 'value' => 'A1', 'reference' => 'A1'],
114+
// ['type' => 'Cell Reference', 'value' => 'C3', 'reference' => 'C3'],
115+
// ['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
116+
// ['type' => 'Cell Reference', 'value' => 'B2', 'reference' => 'B2'],
117+
// ['type' => 'Cell Reference', 'value' => 'D4', 'reference' => 'D4'],
118+
// ['type' => 'Binary Operator', 'value' => ':', 'reference' => null],
119+
// ['type' => 'Binary Operator', 'value' => '∪', 'reference' => null],
120+
// ],
121+
// '=A1:C3,B2:D4',
122+
// ],
123+
// 'Named Range Union' => [
124+
// [
125+
// ['type' => 'Defined Name', 'value' => 'DEFINED_NAME_1', 'reference' => 'DEFINED_NAME_1'],
126+
// ['type' => 'Defined Name', 'value' => 'DEFINED_NAME_2', 'reference' => 'DEFINED_NAME_2'],
127+
// ['type' => 'Binary Operator', 'value' => '∪', 'reference' => null],
128+
// ],
129+
// '=DEFINED_NAME_1,DEFINED_NAME_2',
130+
// ],
131+
];
132+
}
133+
}

0 commit comments

Comments
 (0)