Failed Conditions
Pull Request — master (#3962)
by Owen
17:41 queued 07:20
created

Unique::uniqueByRow()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 49
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 31
CRAP Score 9

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 49
ccs 31
cts 31
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 6
nop 2
crap 9
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
6
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
7
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
8
9
class Unique
10
{
11
    /**
12
     * UNIQUE
13
     * The UNIQUE function searches for value either from a one-row or one-column range or from an array.
14
     *
15
     * @param mixed $lookupVector The range of cells being searched
16
     * @param mixed $byColumn Whether the uniqueness should be determined by row (the default) or by column
17
     * @param mixed $exactlyOnce Whether the function should return only entries that occur just once in the list
18
     *
19
     * @return mixed The unique values from the search range
20
     */
21 26
    public static function unique(mixed $lookupVector, mixed $byColumn = false, mixed $exactlyOnce = false): mixed
22
    {
23 26
        if (!is_array($lookupVector)) {
24
            // Scalars are always returned "as is"
25 1
            return $lookupVector;
26
        }
27
28 25
        $byColumn = (bool) $byColumn;
29 25
        $exactlyOnce = (bool) $exactlyOnce;
30
31 25
        return ($byColumn === true)
32 8
            ? self::uniqueByColumn($lookupVector, $exactlyOnce)
33 25
            : self::uniqueByRow($lookupVector, $exactlyOnce);
34
    }
35
36 22
    private static function uniqueByRow(array $lookupVector, bool $exactlyOnce): mixed
37
    {
38
        // When not $byColumn, we count whole rows or values, not individual values
39
        //      so implode each row into a single string value
40 22
        array_walk(
41 22
            $lookupVector,
42 22
            function (array &$value): void {
43 22
                $valuex = '';
44 22
                $separator = '';
45 22
                $numericIndicator = "\x01";
46 22
                foreach ($value as $cellValue) {
47 22
                    $valuex .= $separator . $cellValue;
48 22
                    $separator = "\x00";
49 22
                    if (is_int($cellValue) || is_float($cellValue)) {
50 16
                        $valuex .= $numericIndicator;
51
                    }
52
                }
53 22
                $value = $valuex;
54 22
            }
55 22
        );
56
57 22
        $result = self::countValuesCaseInsensitive($lookupVector);
58
59 22
        if ($exactlyOnce === true) {
60 2
            $result = self::exactlyOnceFilter($result);
61
        }
62
63 22
        if (count($result) === 0) {
64 1
            return ExcelError::CALC();
65
        }
66
67 21
        $result = array_keys($result);
68
69
        // restore rows from their strings
70 21
        array_walk(
71 21
            $result,
72 21
            function (string &$value): void {
73 21
                $value = explode("\x00", $value);
74 21
                foreach ($value as &$stringValue) {
75 21
                    if (str_ends_with($stringValue, "\x01")) {
76
                        // x01 should only end a string which is otherwise a float or int,
77
                        // so phpstan is technically correct but what it fears should not happen.
78 16
                        $stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore-line
79
                    }
80
                }
81 21
            }
82 21
        );
83
84 21
        return (count($result) === 1) ? array_pop($result) : $result;
85
    }
86
87 8
    private static function uniqueByColumn(array $lookupVector, bool $exactlyOnce): mixed
88
    {
89 8
        $flattenedLookupVector = Functions::flattenArray($lookupVector);
90
91 8
        if (count($lookupVector, COUNT_RECURSIVE) > count($flattenedLookupVector, COUNT_RECURSIVE) + 1) {
92
            // We're looking at a full column check (multiple rows)
93 1
            $transpose = Matrix::transpose($lookupVector);
94 1
            $result = self::uniqueByRow($transpose, $exactlyOnce);
95
96 1
            return (is_array($result)) ? Matrix::transpose($result) : $result;
97
        }
98
99 7
        $result = self::countValuesCaseInsensitive($flattenedLookupVector);
100
101 7
        if ($exactlyOnce === true) {
102 2
            $result = self::exactlyOnceFilter($result);
103
        }
104
105 7
        if (count($result) === 0) {
106 1
            return ExcelError::CALC();
107
        }
108
109 6
        $result = array_keys($result);
110
111 6
        return $result;
112
    }
113
114 25
    private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array
115
    {
116 25
        $caseInsensitiveCounts = array_count_values(
117 25
            array_map(
118 25
                fn (string $value): string => StringHelper::strToUpper($value),
119 25
                $caseSensitiveLookupValues
120 25
            )
121 25
        );
122
123 25
        $caseSensitiveCounts = [];
124 25
        foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) {
125 25
            if (is_numeric($caseInsensitiveKey)) {
126 5
                $caseSensitiveCounts[$caseInsensitiveKey] = $count;
127
            } else {
128 24
                foreach ($caseSensitiveLookupValues as $caseSensitiveValue) {
129 24
                    if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) {
130 24
                        $caseSensitiveCounts[$caseSensitiveValue] = $count;
131
132 24
                        break;
133
                    }
134
                }
135
            }
136
        }
137
138 25
        return $caseSensitiveCounts;
139
    }
140
141 3
    private static function exactlyOnceFilter(array $values): array
142
    {
143 3
        return array_filter(
144 3
            $values,
145 3
            fn ($value): bool => $value === 1
146 3
        );
147
    }
148
}
149