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