Passed
Pull Request — master (#4382)
by Owen
13:59
created

Conditional   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 308
Duplicated Lines 0 %

Test Coverage

Coverage 95.15%

Importance

Changes 0
Metric Value
wmc 44
eloc 106
c 0
b 0
f 0
dl 0
loc 308
rs 8.8798
ccs 98
cts 103
cp 0.9515

15 Methods

Rating   Name   Duplication   Size   Complexity  
A buildDataSet() 0 13 2
A SUMIFS() 0 12 3
A AVERAGEIF() 0 14 6
A MINIFS() 0 10 2
A buildDatabase() 0 5 1
A MAXIFS() 0 10 2
A COUNTIF() 0 22 5
A buildConditionSet() 0 5 1
A buildConditionSetForValueRange() 0 12 2
A AVERAGEIFS() 0 17 6
A buildConditions() 0 12 2
A SUMIF() 0 19 6
A COUNTIFS() 0 12 3
A buildDatabaseWithValueRange() 0 9 1
A databaseFromRangeAndValue() 0 12 2

How to fix   Complexity   

Complex Class

Complex classes like Conditional 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 Conditional, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Database\DAverage;
6
use PhpOffice\PhpSpreadsheet\Calculation\Database\DCount;
7
use PhpOffice\PhpSpreadsheet\Calculation\Database\DMax;
8
use PhpOffice\PhpSpreadsheet\Calculation\Database\DMin;
9
use PhpOffice\PhpSpreadsheet\Calculation\Database\DSum;
10
use PhpOffice\PhpSpreadsheet\Calculation\Exception as CalcException;
11
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
12
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
13
14
class Conditional
15
{
16
    private const CONDITION_COLUMN_NAME = 'CONDITION';
17
    private const VALUE_COLUMN_NAME = 'VALUE';
18
    private const CONDITIONAL_COLUMN_NAME = 'CONDITIONAL %d';
19
20
    /**
21
     * AVERAGEIF.
22
     *
23
     * Returns the average value from a range of cells that contain numbers within the list of arguments
24
     *
25
     * Excel Function:
26
     *        AVERAGEIF(range,condition[, average_range])
27
     *
28
     * @param mixed $range Data values, expect array
29
     * @param null|array|string $condition the criteria that defines which cells will be checked
30
     * @param mixed $averageRange Data values
31 18
     */
32
    public static function AVERAGEIF(mixed $range, null|array|string $condition, mixed $averageRange = []): null|int|float|string
33 18
    {
34 2
        if (!is_array($range) || !is_array($averageRange) || array_key_exists(0, $range) || array_key_exists(0, $averageRange)) {
35
            $refError = ExcelError::REF();
36 18
            if (in_array($refError, [$range, $averageRange], true)) {
37 18
                return $refError;
38
            }
39 18
40
            throw new CalcException('Must specify range of cells, not any kind of literal');
41
        }
42
        $database = self::databaseFromRangeAndValue($range, $averageRange);
43
        $condition = [[self::CONDITION_COLUMN_NAME, self::VALUE_COLUMN_NAME], [$condition, null]];
44
45
        return DAverage::evaluate($database, self::VALUE_COLUMN_NAME, $condition);
46
    }
47
48
    /**
49
     * AVERAGEIFS.
50
     *
51
     * Counts the number of cells that contain numbers within the list of arguments
52 6
     *
53
     * Excel Function:
54 6
     *        AVERAGEIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2]…)
55
     *
56 6
     * @param mixed $args Pairs of Ranges and Criteria
57 2
     */
58
    public static function AVERAGEIFS(mixed ...$args): null|int|float|string
59 4
    {
60 4
        if (empty($args)) {
61
            return 0.0;
62
        } elseif (count($args) === 3) {
63
            return self::AVERAGEIF($args[1], $args[2], $args[0]);
64
        }
65 4
        foreach ($args as $arg) {
66 4
            if (is_array($arg) && array_key_exists(0, $arg)) {
67
                throw new CalcException('Must specify range of cells, not any kind of literal');
68 4
            }
69
        }
70
71
        $conditions = self::buildConditionSetForValueRange(...$args);
72
        $database = self::buildDatabaseWithValueRange(...$args);
73
74
        return DAverage::evaluate($database, self::VALUE_COLUMN_NAME, $conditions);
75
    }
76
77
    /**
78
     * COUNTIF.
79
     *
80
     * Counts the number of cells that contain numbers within the list of arguments
81
     *
82 27
     * Excel Function:
83
     *        COUNTIF(range,condition)
84
     *
85 27
     * @param mixed $range Data values, expect array
86 27
     * @param null|array|string $condition the criteria that defines which cells will be counted
87 27
     */
88 27
    public static function COUNTIF(mixed $range, null|array|string $condition): string|int
89
    {
90 27
        if (
91 27
            !is_array($range)
92
            || array_key_exists(0, $range)
93 27
        ) {
94
            if ($range === ExcelError::REF()) {
95
                return $range;
96
            }
97
98
            throw new CalcException('Must specify range of cells, not any kind of literal');
99
        }
100
        // Filter out any empty values that shouldn't be included in a COUNT
101
        $range = array_filter(
102
            Functions::flattenArray($range),
103
            fn ($value): bool => $value !== null && $value !== ''
104
        );
105
106 6
        $range = array_merge([[self::CONDITION_COLUMN_NAME]], array_chunk($range, 1));
107
        $condition = array_merge([[self::CONDITION_COLUMN_NAME]], [[$condition]]);
108 6
109
        return DCount::evaluate($range, null, $condition, false);
110 6
    }
111 3
112
    /**
113
     * COUNTIFS.
114 3
     *
115 3
     * Counts the number of cells that contain numbers within the list of arguments
116
     *
117 3
     * Excel Function:
118
     *        COUNTIFS(criteria_range1, criteria1, [criteria_range2, criteria2]…)
119
     *
120
     * @param mixed $args Pairs of Ranges and Criteria
121
     */
122
    public static function COUNTIFS(mixed ...$args): int|string
123
    {
124
        if (empty($args)) {
125
            return 0;
126
        } elseif (count($args) === 2) {
127
            return self::COUNTIF(...$args);
128
        }
129
130 5
        $database = self::buildDatabase(...$args);
131
        $conditions = self::buildConditionSet(...$args);
132 5
133
        return DCount::evaluate($database, null, $conditions, false);
134
    }
135
136 5
    /**
137 5
     * MAXIFS.
138
     *
139 5
     * Returns the maximum value within a range of cells that contain numbers within the list of arguments
140
     *
141
     * Excel Function:
142
     *        MAXIFS(max_range, criteria_range1, criteria1, [criteria_range2, criteria2]…)
143
     *
144
     * @param mixed $args Pairs of Ranges and Criteria
145
     */
146
    public static function MAXIFS(mixed ...$args): null|float|string
147
    {
148
        if (empty($args)) {
149
            return 0.0;
150
        }
151
152 5
        $conditions = self::buildConditionSetForValueRange(...$args);
153
        $database = self::buildDatabaseWithValueRange(...$args);
154 5
155
        return DMax::evaluate($database, self::VALUE_COLUMN_NAME, $conditions, false);
156
    }
157
158 5
    /**
159 5
     * MINIFS.
160
     *
161 5
     * Returns the minimum value within a range of cells that contain numbers within the list of arguments
162
     *
163
     * Excel Function:
164
     *        MINIFS(min_range, criteria_range1, criteria1, [criteria_range2, criteria2]…)
165
     *
166
     * @param mixed $args Pairs of Ranges and Criteria
167
     */
168
    public static function MINIFS(mixed ...$args): null|float|string
169
    {
170
        if (empty($args)) {
171
            return 0.0;
172
        }
173
174 22
        $conditions = self::buildConditionSetForValueRange(...$args);
175
        $database = self::buildDatabaseWithValueRange(...$args);
176 22
177 22
        return DMin::evaluate($database, self::VALUE_COLUMN_NAME, $conditions, false);
178
    }
179 22
180
    /**
181
     * SUMIF.
182
     *
183
     * Totals the values of cells that contain numbers within the list of arguments
184
     *
185
     * Excel Function:
186
     *        SUMIF(range, criteria, [sum_range])
187
     *
188
     * @param mixed $range Data values, expecting array
189
     * @param mixed $sumRange Data values, expecting array
190
     */
191
    public static function SUMIF(mixed $range, mixed $condition, mixed $sumRange = []): null|float|string
192 6
    {
193
        if (
194 6
            !is_array($range)
195 1
            || array_key_exists(0, $range)
196 5
            || !is_array($sumRange)
197 1
            || array_key_exists(0, $sumRange)
198
        ) {
199
            $refError = ExcelError::REF();
200 4
            if (in_array($refError, [$range, $sumRange], true)) {
201 4
                return $refError;
202
            }
203 4
204
            throw new CalcException('Must specify range of cells, not any kind of literal');
205
        }
206
        $database = self::databaseFromRangeAndValue($range, $sumRange);
207 3
        $condition = [[self::CONDITION_COLUMN_NAME, self::VALUE_COLUMN_NAME], [$condition, null]];
208
209 3
        return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $condition);
210
    }
211 3
212
    /**
213
     * SUMIFS.
214
     *
215 18
     * Counts the number of cells that contain numbers within the list of arguments
216
     *
217 18
     * Excel Function:
218
     *        SUMIFS(average_range, criteria_range1, criteria1, [criteria_range2, criteria2]…)
219 18
     *
220 4
     * @param mixed $args Pairs of Ranges and Criteria
221 4
     */
222 4
    public static function SUMIFS(mixed ...$args): null|float|string
223 4
    {
224
        if (empty($args)) {
225
            return 0.0;
226 14
        } elseif (count($args) === 3) {
227
            return self::SUMIF($args[1], $args[2], $args[0]);
228
        }
229
230 21
        $conditions = self::buildConditionSetForValueRange(...$args);
231
        $database = self::buildDatabaseWithValueRange(...$args);
232 21
233
        return DSum::evaluate($database, self::VALUE_COLUMN_NAME, $conditions);
234 21
    }
235 21
236 21
    /** @param array $args */
237 21
    private static function buildConditionSet(...$args): array
238 21
    {
239
        $conditions = self::buildConditions(1, ...$args);
240
241 21
        return array_map(null, ...$conditions);
242
    }
243
244
    /** @param array $args */
245 3
    private static function buildConditionSetForValueRange(...$args): array
246
    {
247 3
        $conditions = self::buildConditions(2, ...$args);
248
249 3
        if (count($conditions) === 1) {
250
            return array_map(
251
                fn ($value): array => [$value],
252
                $conditions[0]
253 18
            );
254
        }
255 18
256 18
        return array_map(null, ...$conditions);
257 18
    }
258 18
259 18
    /** @param array $args */
260
    private static function buildConditions(int $startOffset, ...$args): array
261 18
    {
262
        $conditions = [];
263
264
        $pairCount = 1;
265 21
        $argumentCount = count($args);
266
        for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) {
267 21
            $conditions[] = array_merge([sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)], [$args[$argument]]);
268 21
            ++$pairCount;
269 21
        }
270 21
271 21
        return $conditions;
272 21
    }
273 21
274 21
    /** @param array $args */
275
    private static function buildDatabase(...$args): array
276
    {
277 21
        $database = [];
278
279
        return self::buildDataSet(0, $database, ...$args);
280 40
    }
281
282 40
    /** @param array $args */
283
    private static function buildDatabaseWithValueRange(...$args): array
284 40
    {
285 40
        $database = [];
286 7
        $database[] = array_merge(
287
            [self::VALUE_COLUMN_NAME],
288
            Functions::flattenArray($args[0])
289 40
        );
290
291 40
        return self::buildDataSet(1, $database, ...$args);
292
    }
293
294
    /** @param array $args */
295
    private static function buildDataSet(int $startOffset, array $database, ...$args): array
296
    {
297
        $pairCount = 1;
298
        $argumentCount = count($args);
299
        for ($argument = $startOffset; $argument < $argumentCount; $argument += 2) {
300
            $database[] = array_merge(
301
                [sprintf(self::CONDITIONAL_COLUMN_NAME, $pairCount)],
302
                Functions::flattenArray($args[$argument])
303
            );
304
            ++$pairCount;
305
        }
306
307
        return array_map(null, ...$database);
308
    }
309
310
    private static function databaseFromRangeAndValue(array $range, array $valueRange = []): array
311
    {
312
        $range = Functions::flattenArray($range);
313
314
        $valueRange = Functions::flattenArray($valueRange);
315
        if (empty($valueRange)) {
316
            $valueRange = $range;
317
        }
318
319
        $database = array_map(null, array_merge([self::CONDITION_COLUMN_NAME], $range), array_merge([self::VALUE_COLUMN_NAME], $valueRange));
320
321
        return $database;
322
    }
323
}
324