Completed
Push — master ( 414e56...4f6d4a )
by Adrien
09:01
created

providerCanLoadAllSupportedLocales()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 21
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 19
nc 1
nop 0
dl 0
loc 21
rs 9.6333
c 0
b 0
f 0
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheetTests\Calculation;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
7
use PhpOffice\PhpSpreadsheet\Spreadsheet;
8
use PHPUnit\Framework\TestCase;
9
10
class CalculationTest extends TestCase
11
{
12
    protected function setUp(): void
13
    {
14
        Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
15
    }
16
17
    protected function tearDown(): void
18
    {
19
        $calculation = Calculation::getInstance();
20
        $calculation->setLocale('en_us');
21
    }
22
23
    /**
24
     * @dataProvider providerBinaryComparisonOperation
25
     *
26
     * @param mixed $formula
27
     * @param mixed $expectedResultExcel
28
     * @param mixed $expectedResultOpenOffice
29
     */
30
    public function testBinaryComparisonOperation($formula, $expectedResultExcel, $expectedResultOpenOffice)
31
    {
32
        Functions::setCompatibilityMode(Functions::COMPATIBILITY_EXCEL);
33
        $resultExcel = Calculation::getInstance()->_calculateFormulaValue($formula);
34
        self::assertEquals($expectedResultExcel, $resultExcel, 'should be Excel compatible');
35
36
        Functions::setCompatibilityMode(Functions::COMPATIBILITY_OPENOFFICE);
37
        $resultOpenOffice = Calculation::getInstance()->_calculateFormulaValue($formula);
38
        self::assertEquals($expectedResultOpenOffice, $resultOpenOffice, 'should be OpenOffice compatible');
39
    }
40
41
    public function providerBinaryComparisonOperation()
42
    {
43
        return require 'tests/data/CalculationBinaryComparisonOperation.php';
44
    }
45
46
    /**
47
     * @dataProvider providerGetFunctions
48
     *
49
     * @param string $category
50
     * @param array|string $functionCall
51
     * @param string $argumentCount
52
     */
53
    public function testGetFunctions($category, $functionCall, $argumentCount)
0 ignored issues
show
Unused Code introduced by
The parameter $argumentCount is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

53
    public function testGetFunctions($category, $functionCall, /** @scrutinizer ignore-unused */ $argumentCount)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $category is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

53
    public function testGetFunctions(/** @scrutinizer ignore-unused */ $category, $functionCall, $argumentCount)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
54
    {
55
        self::assertIsCallable($functionCall);
56
    }
57
58
    public function providerGetFunctions()
59
    {
60
        return Calculation::getInstance()->getFunctions();
61
    }
62
63
    public function testIsImplemented()
64
    {
65
        $calculation = Calculation::getInstance();
66
        self::assertFalse($calculation->isImplemented('non-existing-function'));
67
        self::assertFalse($calculation->isImplemented('AREAS'));
68
        self::assertTrue($calculation->isImplemented('coUNt'));
69
        self::assertTrue($calculation->isImplemented('abs'));
70
    }
71
72
    /**
73
     * @dataProvider providerCanLoadAllSupportedLocales
74
     *
75
     * @param string $locale
76
     */
77
    public function testCanLoadAllSupportedLocales($locale)
78
    {
79
        $calculation = Calculation::getInstance();
80
        self::assertTrue($calculation->setLocale($locale));
81
    }
82
83
    public function providerCanLoadAllSupportedLocales()
84
    {
85
        return [
86
            ['bg'],
87
            ['cs'],
88
            ['da'],
89
            ['de'],
90
            ['en_us'],
91
            ['es'],
92
            ['fi'],
93
            ['fr'],
94
            ['hu'],
95
            ['it'],
96
            ['nl'],
97
            ['no'],
98
            ['pl'],
99
            ['pt'],
100
            ['pt_br'],
101
            ['ru'],
102
            ['sv'],
103
            ['tr'],
104
        ];
105
    }
106
107
    public function testDoesHandleXlfnFunctions()
108
    {
109
        $calculation = Calculation::getInstance();
110
111
        $tree = $calculation->parseFormula('=_xlfn.ISFORMULA(A1)');
112
        self::assertCount(3, $tree);
1 ignored issue
show
Bug introduced by
It seems like $tree can also be of type boolean; however, parameter $haystack of PHPUnit\Framework\Assert::assertCount() does only seem to accept Countable|iterable, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

112
        self::assertCount(3, /** @scrutinizer ignore-type */ $tree);
Loading history...
113
        $function = $tree[2];
114
        self::assertEquals('Function', $function['type']);
115
116
        $tree = $calculation->parseFormula('=_xlfn.STDEV.S(A1:B2)');
117
        self::assertCount(5, $tree);
118
        $function = $tree[4];
119
        self::assertEquals('Function', $function['type']);
120
    }
121
122
    public function testFormulaWithOptionalArgumentsAndRequiredCellReferenceShouldPassNullForMissingArguments()
123
    {
124
        $spreadsheet = new Spreadsheet();
125
        $sheet = $spreadsheet->getActiveSheet();
126
127
        $sheet->fromArray(
128
            [
129
                [1, 2, 3],
130
                [4, 5, 6],
131
                [7, 8, 9],
132
            ]
133
        );
134
135
        $cell = $sheet->getCell('E5');
136
        $cell->setValue('=OFFSET(D3, -1, -2, 1, 1)');
137
        self::assertEquals(5, $cell->getCalculatedValue(), 'with all arguments');
138
139
        $cell = $sheet->getCell('F6');
140
        $cell->setValue('=OFFSET(D3, -1, -2)');
141
        self::assertEquals(5, $cell->getCalculatedValue(), 'missing arguments should be filled with null');
142
    }
143
144
    public function testCellSetAsQuotedText()
145
    {
146
        $spreadsheet = new Spreadsheet();
147
        $workSheet = $spreadsheet->getActiveSheet();
148
        $cell = $workSheet->getCell('A1');
149
150
        $cell->setValue("=cmd|'/C calc'!A0");
151
        $cell->getStyle()->setQuotePrefix(true);
152
153
        self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
154
    }
155
156
    public function testCellWithDdeExpresion()
157
    {
158
        $spreadsheet = new Spreadsheet();
159
        $workSheet = $spreadsheet->getActiveSheet();
160
        $cell = $workSheet->getCell('A1');
161
162
        $cell->setValue("=cmd|'/C calc'!A0");
163
164
        self::assertEquals("=cmd|'/C calc'!A0", $cell->getCalculatedValue());
165
    }
166
167
    public function testCellWithFormulaTwoIndirect()
168
    {
169
        $spreadsheet = new Spreadsheet();
170
        $workSheet = $spreadsheet->getActiveSheet();
171
        $cell1 = $workSheet->getCell('A1');
172
        $cell1->setValue('2');
173
        $cell2 = $workSheet->getCell('B1');
174
        $cell2->setValue('3');
175
        $cell2 = $workSheet->getCell('C1');
176
        $cell2->setValue('4');
177
        $cell3 = $workSheet->getCell('D1');
178
        $cell3->setValue('=SUM(INDIRECT("A"&ROW()),INDIRECT("B"&ROW()),INDIRECT("C"&ROW()))');
179
180
        self::assertEquals('9', $cell3->getCalculatedValue());
181
    }
182
183
    public function testBranchPruningFormulaParsingSimpleCase()
184
    {
185
        $calculation = Calculation::getInstance();
186
        $calculation->flushInstance(); // resets the ids
187
188
        // Very simple formula
189
        $formula = '=IF(A1="please +",B1)';
190
        $tokens = $calculation->parseFormula($formula);
191
192
        $foundEqualAssociatedToStoreKey = false;
193
        $foundConditionalOnB1 = false;
194
        foreach ($tokens as $token) {
195
            $isBinaryOperator = $token['type'] == 'Binary Operator';
196
            $isEqual = $token['value'] == '=';
197
            $correctStoreKey = ($token['storeKey'] ?? '') == 'storeKey-0';
198
            $correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-0';
199
            $isB1Reference = ($token['reference'] ?? '') == 'B1';
200
201
            $foundEqualAssociatedToStoreKey = $foundEqualAssociatedToStoreKey ||
202
                ($isBinaryOperator && $isEqual && $correctStoreKey);
203
204
            $foundConditionalOnB1 = $foundConditionalOnB1 ||
205
                ($isB1Reference && $correctOnlyIf);
206
        }
207
        $this->assertTrue($foundEqualAssociatedToStoreKey);
208
        $this->assertTrue($foundConditionalOnB1);
209
    }
210
211
    public function testBranchPruningFormulaParsingMultipleIfsCase()
212
    {
213
        $calculation = Calculation::getInstance();
214
        $calculation->flushInstance(); // resets the ids
215
216
        //
217
        // Internal operation
218
        $formula = '=IF(A1="please +",SUM(B1:B3))+IF(A2="please *",PRODUCT(C1:C3), C1)';
219
        $tokens = $calculation->parseFormula($formula);
220
221
        $plusGotTagged = false;
222
        $productFunctionCorrectlyTagged = false;
223
        foreach ($tokens as $token) {
224
            $isBinaryOperator = $token['type'] == 'Binary Operator';
225
            $isPlus = $token['value'] == '+';
226
            $anyStoreKey = isset($token['storeKey']);
227
            $anyOnlyIf = isset($token['onlyIf']);
228
            $anyOnlyIfNot = isset($token['onlyIfNot']);
229
            $plusGotTagged = $plusGotTagged ||
230
                ($isBinaryOperator && $isPlus &&
231
                    ($anyStoreKey || $anyOnlyIfNot || $anyOnlyIf));
232
233
            $isFunction = $token['type'] == 'Function';
234
            $isProductFunction = $token['value'] == 'PRODUCT(';
235
            $correctOnlyIf = ($token['onlyIf'] ?? '') == 'storeKey-1';
236
            $productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isFunction && $isProductFunction && $correctOnlyIf);
237
        }
238
        $this->assertFalse($plusGotTagged, 'chaining IF( should not affect the external operators');
239
        $this->assertTrue($productFunctionCorrectlyTagged, 'function nested inside if should be tagged to be processed only if parent branching requires it');
240
    }
241
242
    public function testBranchPruningFormulaParingNestedIfCase()
243
    {
244
        $calculation = Calculation::getInstance();
245
        $calculation->flushInstance(); // resets the ids
246
247
        $formula = '=IF(A1="please +",SUM(B1:B3),1+IF(NOT(A2="please *"),C2-C1,PRODUCT(C1:C3)))';
248
        $tokens = $calculation->parseFormula($formula);
249
250
        $plusCorrectlyTagged = false;
251
        $productFunctionCorrectlyTagged = false;
252
        $notFunctionCorrectlyTagged = false;
253
        $findOneOperandCountTagged = false;
254
        foreach ($tokens as $token) {
255
            $value = $token['value'];
256
            $isPlus = $value == '+';
257
            $isProductFunction = $value == 'PRODUCT(';
258
            $isNotFunction = $value == 'NOT(';
259
            $isIfOperand = $token['type'] == 'Operand Count for Function IF()';
260
            $isOnlyIfNotDepth1 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-1';
261
            $isStoreKeyDepth1 = (array_key_exists('storeKey', $token) ? $token['storeKey'] : null) == 'storeKey-1';
262
            $isOnlyIfNotDepth0 = (array_key_exists('onlyIfNot', $token) ? $token['onlyIfNot'] : null) == 'storeKey-0';
263
264
            $plusCorrectlyTagged = $plusCorrectlyTagged || ($isPlus && $isOnlyIfNotDepth0);
265
            $notFunctionCorrectlyTagged = $notFunctionCorrectlyTagged || ($isNotFunction && $isOnlyIfNotDepth0 && $isStoreKeyDepth1);
266
            $productFunctionCorrectlyTagged = $productFunctionCorrectlyTagged || ($isProductFunction && $isOnlyIfNotDepth1 && !$isStoreKeyDepth1 && !$isOnlyIfNotDepth0);
267
            $findOneOperandCountTagged = $findOneOperandCountTagged || ($isIfOperand && $isOnlyIfNotDepth0);
268
        }
269
        $this->assertTrue($plusCorrectlyTagged);
270
        $this->assertTrue($productFunctionCorrectlyTagged);
271
        $this->assertTrue($notFunctionCorrectlyTagged);
272
    }
273
274
    public function testBranchPruningFormulaParsingNoArgumentFunctionCase()
275
    {
276
        $calculation = Calculation::getInstance();
277
        $calculation->flushInstance(); // resets the ids
278
279
        $formula = '=IF(AND(TRUE(),A1="please +"),2,3)';
280
        // this used to raise a parser error, we keep it even though we don't
281
        // test the output
282
        $calculation->parseFormula($formula);
283
        self::assertTrue(true);
284
    }
285
286
    public function testBranchPruningFormulaParsingInequalitiesConditionsCase()
287
    {
288
        $calculation = Calculation::getInstance();
289
        $calculation->flushInstance(); // resets the ids
290
291
        $formula = '=IF(A1="flag",IF(A2<10, 0) + IF(A3<10000, 0))';
292
        $tokens = $calculation->parseFormula($formula);
293
        $properlyTaggedPlus = false;
294
        foreach ($tokens as $token) {
295
            $isPlus = $token['value'] === '+';
296
            $hasOnlyIf = !empty($token['onlyIf']);
297
298
            $properlyTaggedPlus = $properlyTaggedPlus ||
299
                ($isPlus && $hasOnlyIf);
300
        }
301
        $this->assertTrue($properlyTaggedPlus);
302
    }
303
304
    /**
305
     * @param $expectedResult
306
     * @param $dataArray
307
     * @param string $formula
308
     * @param string $cellCoordinates where to put the formula
309
     * @param string[] $shouldBeSetInCacheCells coordinates of cells that must
310
     *  be set in cache
311
     * @param string[] $shouldNotBeSetInCacheCells coordinates of cells that must
312
     *  not be set in cache because of pruning
313
     *
314
     * @dataProvider dataProviderBranchPruningFullExecution
315
     */
316
    public function testFullExecution(
317
        $expectedResult,
318
        $dataArray,
319
        $formula,
320
        $cellCoordinates,
321
        $shouldBeSetInCacheCells = [],
322
        $shouldNotBeSetInCacheCells = []
323
    ) {
324
        $spreadsheet = new Spreadsheet();
325
        $sheet = $spreadsheet->getActiveSheet();
326
327
        $sheet->fromArray($dataArray);
328
        $cell = $sheet->getCell($cellCoordinates);
329
        $calculation = Calculation::getInstance($cell->getWorksheet()->getParent());
330
331
        $cell->setValue($formula);
332
        $calculated = $cell->getCalculatedValue();
333
        $this->assertEquals($expectedResult, $calculated);
334
335
        // this mostly to ensure that at least some cells are cached
336
        foreach ($shouldBeSetInCacheCells as $setCell) {
337
            unset($inCache);
338
            $calculation->getValueFromCache('Worksheet!' . $setCell, $inCache);
339
            $this->assertNotEmpty($inCache);
340
        }
341
342
        foreach ($shouldNotBeSetInCacheCells as $notSetCell) {
343
            unset($inCache);
344
            $calculation->getValueFromCache('Worksheet!' . $notSetCell, $inCache);
345
            $this->assertEmpty($inCache);
346
        }
347
348
        $calculation->disableBranchPruning();
349
        $calculated = $cell->getCalculatedValue();
350
        $this->assertEquals($expectedResult, $calculated);
351
    }
352
353
    public function dataProviderBranchPruningFullExecution()
354
    {
355
        return require 'tests/data/Calculation/Calculation.php';
356
    }
357
358
    public function testUnknownFunction(): void
359
    {
360
        $workbook = new Spreadsheet();
361
        $sheet = $workbook->getActiveSheet();
362
        $sheet->setCellValue('A1', '=gzorg()');
363
        $sheet->setCellValue('A2', '=mode.gzorg(1)');
364
        $sheet->setCellValue('A3', '=gzorg(1,2)');
365
        $sheet->setCellValue('A4', '=3+IF(gzorg(),1,2)');
366
        self::assertEquals('#NAME?', $sheet->getCell('A1')->getCalculatedValue());
367
        self::assertEquals('#NAME?', $sheet->getCell('A2')->getCalculatedValue());
368
        self::assertEquals('#NAME?', $sheet->getCell('A3')->getCalculatedValue());
369
        self::assertEquals('#NAME?', $sheet->getCell('A4')->getCalculatedValue());
370
    }
371
}
372