Failed Conditions
Pull Request — master (#3962)
by Owen
11:35
created

CalculationTest   D

Complexity

Total Complexity 58

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 58
eloc 234
dl 0
loc 406
rs 4.5599
c 0
b 0
f 0

22 Methods

Rating   Name   Duplication   Size   Complexity  
A testFormulaReferencingWorksheetWithEscapedApostrophe() 0 17 1
A dataProviderBranchPruningFullExecution() 0 3 1
A testBranchPruningFormulaParsingInequalitiesConditionsCase() 0 18 4
A testFormulaReferencingWorksheetWithUnescapedApostrophe() 0 17 1
A testBinaryComparisonOperation() 0 9 1
A testCellWithStringPercentage() 0 11 1
F testBranchPruningFormulaParingNestedIfCase() 0 31 16
A testFormulaWithOptionalArgumentsAndRequiredCellReferenceShouldPassNullForMissingArguments() 0 21 1
A setUp() 0 4 1
A testCellSetAsQuotedText() 0 33 2
A testDoesHandleXlfnFunctions() 0 15 1
A testCellWithStringFraction() 0 11 1
A testCellWithDdeExpresion() 0 10 1
A providerBinaryComparisonOperation() 0 3 1
A testFullExecutionDataPruning() 0 36 3
B testBranchPruningFormulaParsingSimpleCase() 0 27 7
A testCellWithStringCurrency() 0 13 1
A tearDown() 0 3 1
A testBranchPruningFormulaParsingNoArgumentFunctionCase() 0 10 1
A testCellWithFormulaTwoIndirect() 0 15 1
B testBranchPruningFormulaParsingMultipleIfsCase() 0 30 10
A testCellWithStringNumeric() 0 11 1

How to fix   Complexity   

Complex Class

Complex classes like CalculationTest often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use CalculationTest, and based on these observations, apply Extract Interface, too.

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