testBranchPruningFormulaParingNestedIfCase()   F
last analyzed

Complexity

Conditions 16
Paths 1441

Size

Total Lines 31
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 16
eloc 25
nc 1441
nop 0
dl 0
loc 31
rs 1.4
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
6
7
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
8
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
9
use PhpOffice\PhpSpreadsheet\Cell\DataType;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
use PhpOffice\PhpSpreadsheet\Spreadsheet;
12
use PHPUnit\Framework\TestCase;
13
14
class CalculationTest extends TestCase
15
{
16
    private string $compatibilityMode;
17
18
    private string $locale;
19
20
    protected function setUp(): void
21
    {
22
        $this->compatibilityMode = Functions::getCompatibilityMode();
23
        $calculation = Calculation::getInstance();
24
        $this->locale = $calculation->getLocale();
25
        Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
26
    }
27
28
    protected function tearDown(): void
29
    {
30
        Functions::setCompatibilityMode($this->compatibilityMode);
31
        $calculation = Calculation::getInstance();
32
        $calculation->setLocale($this->locale);
33
    }
34
35
    /**
36
     * @dataProvider providerBinaryComparisonOperation
37
     */
38
    public function testBinaryComparisonOperation(string $formula, mixed $expectedResultExcel, mixed $expectedResultOpenOffice): void
39
    {
40
        Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
41
        $resultExcel = Calculation::getInstance()->_calculateFormulaValue($formula);
42
        self::assertEquals($expectedResultExcel, $resultExcel, 'should be Excel compatible');
43
44
        Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE);
45
        $resultOpenOffice = Calculation::getInstance()->_calculateFormulaValue($formula);
46
        self::assertEquals($expectedResultOpenOffice, $resultOpenOffice, 'should be OpenOffice compatible');
47
    }
48
49
    public static function providerBinaryComparisonOperation(): array
50
    {
51
        return require 'tests/data/CalculationBinaryComparisonOperation.php';
52
    }
53
54
    public function testDoesHandleXlfnFunctions(): void
55
    {
56
        $calculation = Calculation::getInstance();
57
58
        $tree = $calculation->parseFormula('=_xlfn.ISFORMULA(A1)');
59
        self::assertIsArray($tree);
60
        self::assertCount(3, $tree);
61
        $function = $tree[2];
62
        self::assertEquals('Function', $function['type']);
63
64
        $tree = $calculation->parseFormula('=_xlfn.STDEV.S(A1:B2)');
65
        self::assertIsArray($tree);
66
        self::assertCount(5, $tree);
67
        $function = $tree[4];
68
        self::assertEquals('Function', $function['type']);
69
    }
70
71
    public function testFormulaWithOptionalArgumentsAndRequiredCellReferenceShouldPassNullForMissingArguments(): void
72
    {
73
        $spreadsheet = new Spreadsheet();
74
        $sheet = $spreadsheet->getActiveSheet();
75
76
        $sheet->fromArray(
77
            [
78
                [1, 2, 3],
79
                [4, 5, 6],
80
                [7, 8, 9],
81
            ]
82
        );
83
84
        $cell = $sheet->getCell('E5');
85
        $cell->setValue('=OFFSET(D3, -1, -2, 1, 1)');
86
        self::assertEquals(5, $cell->getCalculatedValue(), 'with all arguments');
87
88
        $cell = $sheet->getCell('F6');
89
        $cell->setValue('=OFFSET(D3, -1, -2)');
90
        self::assertEquals(5, $cell->getCalculatedValue(), 'missing arguments should be filled with null');
91
    }
92
93
    public function testCellSetAsQuotedText(): void
94
    {
95
        $spreadsheet = new Spreadsheet();
96
        $workSheet = $spreadsheet->getActiveSheet();
97
        $cell = $workSheet->getCell('A1');
98
99
        $cell->setValue("=cmd|'/C calc'!A0");
100
        $cell->getStyle()->setQuotePrefix(true);
101
102
        self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
103
104
        $cell2 = $workSheet->getCell('A2');
105
        $cell2->setValueExplicit('ABC', DataType::TYPE_FORMULA);
106
        self::assertEquals('ABC', $cell2->getCalculatedValue());
107
108
        $cell3 = $workSheet->getCell('A3');
109
        $cell3->setValueExplicit('=', DataType::TYPE_FORMULA);
110
        self::assertEquals('', $cell3->getCalculatedValue());
111
    }
112
113
    public function testCellWithDdeExpresion(): void
114
    {
115
        $spreadsheet = new Spreadsheet();
116
        $workSheet = $spreadsheet->getActiveSheet();
117
        $cell = $workSheet->getCell('A1');
118
119
        $cell->setValue("=cmd|'/C calc'!A0");
120
121
        self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
122
    }
123
124
    public function testFormulaReferencingWorksheetWithEscapedApostrophe(): void
125
    {
126
        $spreadsheet = new Spreadsheet();
127
        $workSheet = $spreadsheet->getActiveSheet();
128
        $workSheet->setTitle("Catégorie d'absence");
129
130
        $workSheet->setCellValue('A1', 'HELLO');
131
        $workSheet->setCellValue('B1', ' ');
132
        $workSheet->setCellValue('C1', 'WORLD');
133
        $workSheet->setCellValue(
134
            'A2',
135
            "=CONCAT('Catégorie d''absence'!A1, 'Catégorie d''absence'!B1, 'Catégorie d''absence'!C1)"
136
        );
137
138
        $cellValue = $workSheet->getCell('A2')->getCalculatedValue();
139
        self::assertSame('HELLO WORLD', $cellValue);
140
    }
141
142
    public function testFormulaReferencingWorksheetWithUnescapedApostrophe(): void
143
    {
144
        $spreadsheet = new Spreadsheet();
145
        $workSheet = $spreadsheet->getActiveSheet();
146
        $workSheet->setTitle("Catégorie d'absence");
147
148
        $workSheet->setCellValue('A1', 'HELLO');
149
        $workSheet->setCellValue('B1', ' ');
150
        $workSheet->setCellValue('C1', 'WORLD');
151
        $workSheet->setCellValue(
152
            'A2',
153
            "=CONCAT('Catégorie d'absence'!A1, 'Catégorie d'absence'!B1, 'Catégorie d'absence'!C1)"
154
        );
155
156
        $cellValue = $workSheet->getCell('A2')->getCalculatedValue();
157
        self::assertSame('HELLO WORLD', $cellValue);
158
    }
159
160
    public function testCellWithFormulaTwoIndirect(): void
161
    {
162
        $spreadsheet = new Spreadsheet();
163
        $workSheet = $spreadsheet->getActiveSheet();
164
        $cell1 = $workSheet->getCell('A1');
165
        $cell1->setValue('2');
166
        $cell2 = $workSheet->getCell('B1');
167
        $cell2->setValue('3');
168
        $cell2 = $workSheet->getCell('C1');
169
        $cell2->setValue('4');
170
        $cell3 = $workSheet->getCell('D1');
171
        $cell3->setValue('=SUM(INDIRECT("A"&ROW()),INDIRECT("B"&ROW()),INDIRECT("C"&ROW()))');
172
173
        self::assertEquals('9', $cell3->getCalculatedValue());
174
    }
175
176
    public function testCellWithStringNumeric(): void
177
    {
178
        $spreadsheet = new Spreadsheet();
179
        $workSheet = $spreadsheet->getActiveSheet();
180
        $cell1 = $workSheet->getCell('A1');
181
        $cell1->setValue('+2.5');
182
        $cell2 = $workSheet->getCell('B1');
183
        $cell2->setValue('=100*A1');
184
185
        self::assertSame(250.0, $cell2->getCalculatedValue());
186
    }
187
188
    public function testCellWithStringFraction(): void
189
    {
190
        $spreadsheet = new Spreadsheet();
191
        $workSheet = $spreadsheet->getActiveSheet();
192
        $cell1 = $workSheet->getCell('A1');
193
        $cell1->setValue('3/4');
194
        $cell2 = $workSheet->getCell('B1');
195
        $cell2->setValue('=100*A1');
196
197
        self::assertSame(75.0, $cell2->getCalculatedValue());
198
    }
199
200
    public function testCellWithStringPercentage(): void
201
    {
202
        $spreadsheet = new Spreadsheet();
203
        $workSheet = $spreadsheet->getActiveSheet();
204
        $cell1 = $workSheet->getCell('A1');
205
        $cell1->setValue('2%');
206
        $cell2 = $workSheet->getCell('B1');
207
        $cell2->setValue('=100*A1');
208
209
        self::assertSame(2.0, $cell2->getCalculatedValue());
210
    }
211
212
    public function testCellWithStringCurrency(): void
213
    {
214
        $currencyCode = StringHelper::getCurrencyCode();
215
216
        $spreadsheet = new Spreadsheet();
217
        $workSheet = $spreadsheet->getActiveSheet();
218
        $cell1 = $workSheet->getCell('A1');
219
        $cell1->setValue($currencyCode . '2');
220
        $cell2 = $workSheet->getCell('B1');
221
        $cell2->setValue('=100*A1');
222
223
        self::assertSame(200.0, $cell2->getCalculatedValue());
224
    }
225
226
    public function testBranchPruningFormulaParsingSimpleCase(): void
227
    {
228
        $calculation = Calculation::getInstance();
229
        $calculation->flushInstance(); // resets the ids
230
231
        // Very simple formula
232
        $formula = '=IF(A1="please +",B1)';
233
        $tokens = $calculation->parseFormula($formula);
234
        self::assertIsArray($tokens);
235
236
        $foundEqualAssociatedToStoreKey = false;
237
        $foundConditionalOnB1 = false;
238
        foreach ($tokens as $token) {
239
            $isBinaryOperator = $token['type'] == 'Binary Operator';
240
            $isEqual = $token['value'] == '=';
241
            $correctStoreKey = ($token['storeKey'] ?? '') == 'storeKey-0';
242
            $correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-0';
243
            $isB1Reference = ($token['reference'] ?? '') == 'B1';
244
245
            $foundEqualAssociatedToStoreKey = $foundEqualAssociatedToStoreKey
246
                || ($isBinaryOperator && $isEqual && $correctStoreKey);
247
248
            $foundConditionalOnB1 = $foundConditionalOnB1
249
                || ($isB1Reference && $correctOnlyIf);
250
        }
251
        self::assertTrue($foundEqualAssociatedToStoreKey);
252
        self::assertTrue($foundConditionalOnB1);
253
    }
254
255
    public function testBranchPruningFormulaParsingMultipleIfsCase(): void
256
    {
257
        $calculation = Calculation::getInstance();
258
        $calculation->flushInstance(); // resets the ids
259
260
        //
261
        // Internal operation
262
        $formula = '=IF(A1="please +",SUM(B1:B3))+IF(A2="please *",PRODUCT(C1:C3), C1)';
263
        $tokens = $calculation->parseFormula($formula);
264
        self::assertIsArray($tokens);
265
266
        $plusGotTagged = false;
267
        $productFunctionCorrectlyTagged = false;
268
        foreach ($tokens as $token) {
269
            $isBinaryOperator = $token['type'] == 'Binary Operator';
270
            $isPlus = $token['value'] == '+';
271
            $anyStoreKey = isset($token['storeKey']);
272
            $anyOnlyIf = isset($token['onlyIf']);
273
            $anyOnlyIfNot = isset($token['onlyIfNot']);
274
            $plusGotTagged = $plusGotTagged
275
                || ($isBinaryOperator && $isPlus
276
                    && ($anyStoreKey || $anyOnlyIfNot || $anyOnlyIf));
277
278
            $isFunction = $token['type'] == 'Function';
279
            $isProductFunction = $token['value'] == 'PRODUCT(';
280
            $correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-1';
281
            $productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isFunction && $isProductFunction && $correctOnlyIf);
282
        }
283
        self::assertFalse($plusGotTagged, 'chaining IF( should not affect the external operators');
284
        self::assertTrue($productFunctionCorrectlyTagged, 'function nested inside if should be tagged to be processed only if parent branching requires it');
285
    }
286
287
    public function testBranchPruningFormulaParingNestedIfCase(): void
288
    {
289
        $calculation = Calculation::getInstance();
290
        $calculation->flushInstance(); // resets the ids
291
292
        $formula = '=IF(A1="please +",SUM(B1:B3),1+IF(NOT(A2="please *"),C2-C1,PRODUCT(C1:C3)))';
293
        $tokens = $calculation->parseFormula($formula);
294
        self::assertIsArray($tokens);
295
296
        $plusCorrectlyTagged = false;
297
        $productFunctionCorrectlyTagged = false;
298
        $notFunctionCorrectlyTagged = false;
299
        $findOneOperandCountTagged = false;
300
        foreach ($tokens as $token) {
301
            $value = $token['value'];
302
            $isPlus = $value == '+';
303
            $isProductFunction = $value == 'PRODUCT(';
304
            $isNotFunction = $value == 'NOT(';
305
            $isIfOperand = $token['type'] == 'Operand Count for Function IF()';
306
            $isOnlyIfNotDepth1 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-1';
307
            $isStoreKeyDepth1 = (array_key_exists('storeKey', $token) ? $token['storeKey'] : null) == 'storeKey-1';
308
            $isOnlyIfNotDepth0 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-0';
309
310
            $plusCorrectlyTagged = $plusCorrectlyTagged || ($isPlus && $isOnlyIfNotDepth0);
311
            $notFunctionCorrectlyTagged = $notFunctionCorrectlyTagged || ($isNotFunction && $isOnlyIfNotDepth0 && $isStoreKeyDepth1);
312
            $productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isProductFunction && $isOnlyIfNotDepth1 && !$isStoreKeyDepth1 && !$isOnlyIfNotDepth0);
313
            $findOneOperandCountTagged = $findOneOperandCountTagged || ($isIfOperand && $isOnlyIfNotDepth0);
314
        }
315
        self::assertTrue($plusCorrectlyTagged);
316
        self::assertTrue($productFunctionCorrectlyTagged);
317
        self::assertTrue($notFunctionCorrectlyTagged);
318
    }
319
320
    public function testBranchPruningFormulaParsingNoArgumentFunctionCase(): void
321
    {
322
        $calculation = Calculation::getInstance();
323
        $calculation->flushInstance(); // resets the ids
324
325
        $formula = '=IF(AND(TRUE(),A1="please +"),2,3)';
326
        // this used to raise a parser error, we keep it even though we don't
327
        // test the output
328
        $calculation->parseFormula($formula);
329
        self::assertTrue(true);
330
    }
331
332
    public function testBranchPruningFormulaParsingInequalitiesConditionsCase(): void
333
    {
334
        $calculation = Calculation::getInstance();
335
        $calculation->flushInstance(); // resets the ids
336
337
        $formula = '=IF(A1="flag",IF(A2<10, 0) + IF(A3<10000, 0))';
338
        $tokens = $calculation->parseFormula($formula);
339
        self::assertIsArray($tokens);
340
341
        $properlyTaggedPlus = false;
342
        foreach ($tokens as $token) {
343
            $isPlus = $token['value'] === '+';
344
            $hasOnlyIf = !empty($token['onlyIf']);
345
346
            $properlyTaggedPlus = $properlyTaggedPlus
347
                || ($isPlus && $hasOnlyIf);
348
        }
349
        self::assertTrue($properlyTaggedPlus);
350
    }
351
352
    /**
353
     * @param string $cellCoordinates where to put the formula
354
     * @param string[] $shouldBeSetInCacheCells coordinates of cells that must
355
     *  be set in cache
356
     * @param string[] $shouldNotBeSetInCacheCells coordinates of cells that must
357
     *  not be set in cache because of pruning
358
     *
359
     * @dataProvider dataProviderBranchPruningFullExecution
360
     */
361
    public function testFullExecutionDataPruning(
362
        mixed $expectedResult,
363
        array $dataArray,
364
        string $formula,
365
        string $cellCoordinates,
366
        array $shouldBeSetInCacheCells = [],
367
        array $shouldNotBeSetInCacheCells = []
368
    ): void {
369
        $spreadsheet = new Spreadsheet();
370
        $sheet = $spreadsheet->getActiveSheet();
371
372
        $sheet->fromArray($dataArray);
373
        $cell = $sheet->getCell($cellCoordinates);
374
        $calculation = Calculation::getInstance($cell->getWorksheet()->getParent());
375
376
        $cell->setValue($formula);
377
        $calculated = $cell->getCalculatedValue();
378
        self::assertEquals($expectedResult, $calculated);
379
380
        // this mostly to ensure that at least some cells are cached
381
        foreach ($shouldBeSetInCacheCells as $setCell) {
382
            unset($inCache);
383
            $calculation->getValueFromCache('Worksheet!' . $setCell, $inCache);
384
            self::assertNotEmpty($inCache);
385
        }
386
387
        foreach ($shouldNotBeSetInCacheCells as $notSetCell) {
388
            unset($inCache);
389
            $calculation->getValueFromCache('Worksheet!' . $notSetCell, $inCache);
390
            self::assertEmpty($inCache);
391
        }
392
393
        $calculation->disableBranchPruning();
394
        $calculated = $cell->getCalculatedValue();
395
        self::assertEquals($expectedResult, $calculated);
396
    }
397
398
    public static function dataProviderBranchPruningFullExecution(): array
399
    {
400
        return require 'tests/data/Calculation/Calculation.php';
401
    }
402
}
403