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

Unique::uniqueByRow()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 49
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 28
CRAP Score 9

Importance

Changes 0
Metric Value
eloc 26
c 0
b 0
f 0
dl 0
loc 49
rs 8.0555
ccs 28
cts 28
cp 1
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 12
    public static function unique(mixed $lookupVector, mixed $byColumn = false, mixed $exactlyOnce = false): mixed
22
    {
23 12
        if (!is_array($lookupVector)) {
24
            // Scalars are always returned "as is"
25 1
            return $lookupVector;
26
        }
27
28 11
        $byColumn = (bool) $byColumn;
29 11
        $exactlyOnce = (bool) $exactlyOnce;
30
31 11
        return ($byColumn === true)
32 5
            ? self::uniqueByColumn($lookupVector, $exactlyOnce)
33 11
            : self::uniqueByRow($lookupVector, $exactlyOnce);
34
    }
35
36 8
    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 8
        array_walk(
41 8
            $lookupVector,
42 8
            function (array &$value): void {
43 8
                $valuex = '';
44 8
                $separator = '';
45 8
                $numericIndicator = "\x01";
46
                foreach ($value as $cellValue) {
47 8
                    $valuex .= $separator . $cellValue;
48
                    $separator = "\x00";
49 8
                    if (is_int($cellValue) || is_float($cellValue)) {
50 2
                        $valuex .= $numericIndicator;
51
                    }
52
                }
53 8
                $value = $valuex;
54 1
            }
55
        );
56
57 7
        $result = self::countValuesCaseInsensitive($lookupVector);
58
59
        if ($exactlyOnce === true) {
60 7
            $result = self::exactlyOnceFilter($result);
61 7
        }
62 7
63 7
        if (count($result) === 0) {
64 7
            return ExcelError::CALC();
65 7
        }
66
67 7
        $result = array_keys($result);
68
69
        // restore rows from their strings
70 5
        array_walk(
71
            $result,
72 5
            function (string &$value): void {
73
                $value = explode("\x00", $value);
74 5
                foreach ($value as &$stringValue) {
75
                    if (str_ends_with($stringValue, "\x01")) {
76 1
                        // x01 should only end a string which is otherwise a float or int,
77 1
                        // so phpstan is technically correct but what it fears should not happen.
78
                        $stringValue = 0 + substr($stringValue, 0, -1); //@phpstan-ignore-line
79 1
                    }
80
                }
81
            }
82 4
        );
83
84 4
        return (count($result) === 1) ? array_pop($result) : $result;
85 2
    }
86
87
    private static function uniqueByColumn(array $lookupVector, bool $exactlyOnce): mixed
88 4
    {
89 1
        $flattenedLookupVector = Functions::flattenArray($lookupVector);
90
91
        if (count($lookupVector, COUNT_RECURSIVE) > count($flattenedLookupVector, COUNT_RECURSIVE) + 1) {
92 3
            // We're looking at a full column check (multiple rows)
93
            $transpose = Matrix::transpose($lookupVector);
94 3
            $result = self::uniqueByRow($transpose, $exactlyOnce);
95
96
            return (is_array($result)) ? Matrix::transpose($result) : $result;
97 11
        }
98
99 11
        $result = self::countValuesCaseInsensitive($flattenedLookupVector);
100 11
101 11
        if ($exactlyOnce === true) {
102 11
            $result = self::exactlyOnceFilter($result);
103 11
        }
104 11
105
        if (count($result) === 0) {
106 11
            return ExcelError::CALC();
107 11
        }
108 11
109 2
        $result = array_keys($result);
110
111 9
        return $result;
112 9
    }
113 9
114
    private static function countValuesCaseInsensitive(array $caseSensitiveLookupValues): array
115 9
    {
116
        $caseInsensitiveCounts = array_count_values(
117
            array_map(
118
                fn (string $value): string => StringHelper::strToUpper($value),
119
                $caseSensitiveLookupValues
120
            )
121 11
        );
122
123
        $caseSensitiveCounts = [];
124 3
        foreach ($caseInsensitiveCounts as $caseInsensitiveKey => $count) {
125
            if (is_numeric($caseInsensitiveKey)) {
126 3
                $caseSensitiveCounts[$caseInsensitiveKey] = $count;
127 3
            } else {
128 3
                foreach ($caseSensitiveLookupValues as $caseSensitiveValue) {
129 3
                    if ($caseInsensitiveKey === StringHelper::strToUpper($caseSensitiveValue)) {
130
                        $caseSensitiveCounts[$caseSensitiveValue] = $count;
131
132
                        break;
133
                    }
134
                }
135
            }
136
        }
137
138
        return $caseSensitiveCounts;
139
    }
140
141
    private static function exactlyOnceFilter(array $values): array
142
    {
143
        return array_filter(
144
            $values,
145
            fn ($value): bool => $value === 1
146
        );
147
    }
148
}
149