Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1441 ariadna 1
<?php
2
 
3
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
4
 
5
use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
9
use PhpOffice\PhpSpreadsheet\Calculation\Internal\WildcardMatch;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
 
12
class ExcelMatch
13
{
14
    use ArrayEnabled;
15
 
16
    public const MATCHTYPE_SMALLEST_VALUE = -1;
17
    public const MATCHTYPE_FIRST_VALUE = 0;
18
    public const MATCHTYPE_LARGEST_VALUE = 1;
19
 
20
    /**
21
     * MATCH.
22
     *
23
     * The MATCH function searches for a specified item in a range of cells
24
     *
25
     * Excel Function:
26
     *        =MATCH(lookup_value, lookup_array, [match_type])
27
     *
28
     * @param mixed $lookupValue The value that you want to match in lookup_array
29
     * @param mixed $lookupArray The range of cells being searched
30
     * @param mixed $matchType The number -1, 0, or 1. -1 means above, 0 means exact match, 1 means below.
31
     *                         If match_type is 1 or -1, the list has to be ordered.
32
     *
33
     * @return array|float|int|string The relative position of the found item
34
     */
35
    public static function MATCH(mixed $lookupValue, mixed $lookupArray, mixed $matchType = self::MATCHTYPE_LARGEST_VALUE): array|string|int|float
36
    {
37
        if (is_array($lookupValue)) {
38
            return self::evaluateArrayArgumentsIgnore([self::class, __FUNCTION__], 1, $lookupValue, $lookupArray, $matchType);
39
        }
40
 
41
        $lookupArray = Functions::flattenArray($lookupArray);
42
 
43
        try {
44
            // Input validation
45
            self::validateLookupValue($lookupValue);
46
            $matchType = self::validateMatchType($matchType);
47
            self::validateLookupArray($lookupArray);
48
 
49
            $keySet = array_keys($lookupArray);
50
            if ($matchType == self::MATCHTYPE_LARGEST_VALUE) {
51
                // If match_type is 1 the list has to be processed from last to first
52
                $lookupArray = array_reverse($lookupArray);
53
                $keySet = array_reverse($keySet);
54
            }
55
 
56
            $lookupArray = self::prepareLookupArray($lookupArray, $matchType);
57
        } catch (Exception $e) {
58
            return $e->getMessage();
59
        }
60
 
61
        // MATCH() is not case sensitive, so we convert lookup value to be lower cased if it's a string type.
62
        if (is_string($lookupValue)) {
63
            $lookupValue = StringHelper::strToLower($lookupValue);
64
        }
65
 
66
        $valueKey = match ($matchType) {
67
            self::MATCHTYPE_LARGEST_VALUE => self::matchLargestValue($lookupArray, $lookupValue, $keySet),
68
            self::MATCHTYPE_FIRST_VALUE => self::matchFirstValue($lookupArray, $lookupValue),
69
            default => self::matchSmallestValue($lookupArray, $lookupValue),
70
        };
71
 
72
        if ($valueKey !== null) {
73
            return ++$valueKey;
74
        }
75
 
76
        // Unsuccessful in finding a match, return #N/A error value
77
        return ExcelError::NA();
78
    }
79
 
80
    private static function matchFirstValue(array $lookupArray, mixed $lookupValue): int|string|null
81
    {
82
        if (is_string($lookupValue)) {
83
            $valueIsString = true;
84
            $wildcard = WildcardMatch::wildcard($lookupValue);
85
        } else {
86
            $valueIsString = false;
87
            $wildcard = '';
88
        }
89
 
90
        $valueIsNumeric = is_int($lookupValue) || is_float($lookupValue);
91
        foreach ($lookupArray as $i => $lookupArrayValue) {
92
            if (
93
                $valueIsString
94
                && is_string($lookupArrayValue)
95
            ) {
96
                if (WildcardMatch::compare($lookupArrayValue, $wildcard)) {
97
                    return $i; // wildcard match
98
                }
99
            } else {
100
                if ($lookupArrayValue === $lookupValue) {
101
                    return $i; // exact match
102
                }
103
                if (
104
                    $valueIsNumeric
105
                    && (is_float($lookupArrayValue) || is_int($lookupArrayValue))
106
                    && $lookupArrayValue == $lookupValue
107
                ) {
108
                    return $i; // exact match
109
                }
110
            }
111
        }
112
 
113
        return null;
114
    }
115
 
116
    private static function matchLargestValue(array $lookupArray, mixed $lookupValue, array $keySet): mixed
117
    {
118
        if (is_string($lookupValue)) {
119
            if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
120
                $wildcard = WildcardMatch::wildcard($lookupValue);
121
                foreach (array_reverse($lookupArray) as $i => $lookupArrayValue) {
122
                    if (is_string($lookupArrayValue) && WildcardMatch::compare($lookupArrayValue, $wildcard)) {
123
                        return $i;
124
                    }
125
                }
126
            } else {
127
                foreach ($lookupArray as $i => $lookupArrayValue) {
128
                    if ($lookupArrayValue === $lookupValue) {
129
                        return $keySet[$i];
130
                    }
131
                }
132
            }
133
        }
134
        $valueIsNumeric = is_int($lookupValue) || is_float($lookupValue);
135
        foreach ($lookupArray as $i => $lookupArrayValue) {
136
            if ($valueIsNumeric && (is_int($lookupArrayValue) || is_float($lookupArrayValue))) {
137
                if ($lookupArrayValue <= $lookupValue) {
138
                    return array_search($i, $keySet);
139
                }
140
            }
141
            $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
142
            if ($typeMatch && ($lookupArrayValue <= $lookupValue)) {
143
                return array_search($i, $keySet);
144
            }
145
        }
146
 
147
        return null;
148
    }
149
 
150
    private static function matchSmallestValue(array $lookupArray, mixed $lookupValue): int|string|null
151
    {
152
        $valueKey = null;
153
        if (is_string($lookupValue)) {
154
            if (Functions::getCompatibilityMode() === Functions::COMPATIBILITY_OPENOFFICE) {
155
                $wildcard = WildcardMatch::wildcard($lookupValue);
156
                foreach ($lookupArray as $i => $lookupArrayValue) {
157
                    if (is_string($lookupArrayValue) && WildcardMatch::compare($lookupArrayValue, $wildcard)) {
158
                        return $i;
159
                    }
160
                }
161
            }
162
        }
163
 
164
        $valueIsNumeric = is_int($lookupValue) || is_float($lookupValue);
165
        // The basic algorithm is:
166
        // Iterate and keep the highest match until the next element is smaller than the searched value.
167
        // Return immediately if perfect match is found
168
        foreach ($lookupArray as $i => $lookupArrayValue) {
169
            $typeMatch = gettype($lookupValue) === gettype($lookupArrayValue);
170
            $bothNumeric = $valueIsNumeric && (is_int($lookupArrayValue) || is_float($lookupArrayValue));
171
 
172
            if ($lookupArrayValue === $lookupValue) {
173
                // Another "special" case. If a perfect match is found,
174
                // the algorithm gives up immediately
175
                return $i;
176
            }
177
            if ($bothNumeric && $lookupValue == $lookupArrayValue) {
178
                return $i; // exact match, as above
179
            }
180
            if (($typeMatch || $bothNumeric) && $lookupArrayValue >= $lookupValue) {
181
                $valueKey = $i;
182
            } elseif ($typeMatch && $lookupArrayValue < $lookupValue) {
183
                //Excel algorithm gives up immediately if the first element is smaller than the searched value
184
                break;
185
            }
186
        }
187
 
188
        return $valueKey;
189
    }
190
 
191
    private static function validateLookupValue(mixed $lookupValue): void
192
    {
193
        // Lookup_value type has to be number, text, or logical values
194
        if ((!is_numeric($lookupValue)) && (!is_string($lookupValue)) && (!is_bool($lookupValue))) {
195
            throw new Exception(ExcelError::NA());
196
        }
197
    }
198
 
199
    private static function validateMatchType(mixed $matchType): int
200
    {
201
        // Match_type is 0, 1 or -1
202
        // However Excel accepts other numeric values,
203
        //  including numeric strings and floats.
204
        //  It seems to just be interested in the sign.
205
        if (!is_numeric($matchType)) {
206
            throw new Exception(ExcelError::Value());
207
        }
208
        if ($matchType > 0) {
209
            return self::MATCHTYPE_LARGEST_VALUE;
210
        }
211
        if ($matchType < 0) {
212
            return self::MATCHTYPE_SMALLEST_VALUE;
213
        }
214
 
215
        return self::MATCHTYPE_FIRST_VALUE;
216
    }
217
 
218
    private static function validateLookupArray(array $lookupArray): void
219
    {
220
        // Lookup_array should not be empty
221
        $lookupArraySize = count($lookupArray);
222
        if ($lookupArraySize <= 0) {
223
            throw new Exception(ExcelError::NA());
224
        }
225
    }
226
 
227
    private static function prepareLookupArray(array $lookupArray, mixed $matchType): array
228
    {
229
        // Lookup_array should contain only number, text, or logical values, or empty (null) cells
230
        foreach ($lookupArray as $i => $value) {
231
            //    check the type of the value
232
            if ((!is_numeric($value)) && (!is_string($value)) && (!is_bool($value)) && ($value !== null)) {
233
                throw new Exception(ExcelError::NA());
234
            }
235
            // Convert strings to lowercase for case-insensitive testing
236
            if (is_string($value)) {
237
                $lookupArray[$i] = StringHelper::strToLower($value);
238
            }
239
            if (
240
                ($value === null)
241
                && (($matchType == self::MATCHTYPE_LARGEST_VALUE) || ($matchType == self::MATCHTYPE_SMALLEST_VALUE))
242
            ) {
243
                unset($lookupArray[$i]);
244
            }
245
        }
246
 
247
        return $lookupArray;
248
    }
249
}