Passed
Push — master ( 7288b4...f131ca )
by Adrien
13:07
created

Sort::applyPHP7Patch()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.2559

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 8
ccs 3
cts 5
cp 0.6
rs 10
cc 2
nc 2
nop 2
crap 2.2559
1
<?php
2
3
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
4
5
use PhpOffice\PhpSpreadsheet\Calculation\Calculation;
6
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
7
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
8
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
9
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
10
use PhpOffice\PhpSpreadsheet\Shared\StringHelper;
11
12
class Sort extends LookupRefValidations
13
{
14
    public const ORDER_ASCENDING = 1;
15
    public const ORDER_DESCENDING = -1;
16
17
    /**
18
     * SORT
19
     * The SORT function returns a sorted array of the elements in an array.
20
     * The returned array is the same shape as the provided array argument.
21
     * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
22
     *
23
     * @param mixed $sortArray The range of cells being sorted
24
     * @param mixed $sortIndex The column or row number within the sortArray to sort on
25
     * @param mixed $sortOrder Flag indicating whether to sort ascending or descending
26
     *                          Ascending = 1 (self::ORDER_ASCENDING)
27
     *                          Descending = -1 (self::ORDER_DESCENDING)
28
     * @param mixed $byColumn Whether the sort should be determined by row (the default) or by column
29
     *
30
     * @return mixed The sorted values from the sort range
31
     */
32 22
    public static function sort($sortArray, $sortIndex = 1, $sortOrder = self::ORDER_ASCENDING, $byColumn = false)
33
    {
34 22
        if (!is_array($sortArray)) {
35
            // Scalars are always returned "as is"
36 1
            return $sortArray;
37
        }
38
39 21
        $sortArray = self::enumerateArrayKeys($sortArray);
40
41 21
        $byColumn = (bool) $byColumn;
42 21
        $lookupIndexSize = $byColumn ? count($sortArray) : count($sortArray[0]);
43
44
        try {
45
            // If $sortIndex and $sortOrder are scalars, then convert them into arrays
46 21
            if (is_scalar($sortIndex)) {
47 14
                $sortIndex = [$sortIndex];
48 14
                $sortOrder = is_scalar($sortOrder) ? [$sortOrder] : $sortOrder;
49
            }
50
            // but the values of those array arguments still need validation
51 21
            $sortOrder = (empty($sortOrder) ? [self::ORDER_ASCENDING] : $sortOrder);
52 21
            self::validateArrayArgumentsForSort($sortIndex, $sortOrder, $lookupIndexSize);
53 13
        } catch (Exception $e) {
54 13
            return $e->getMessage();
55
        }
56
57
        // We want a simple, enumrated array of arrays where we can reference column by its index number.
58 8
        $sortArray = array_values(array_map('array_values', $sortArray));
59
60 8
        return ($byColumn === true)
61 2
            ? self::sortByColumn($sortArray, $sortIndex, $sortOrder)
62 8
            : self::sortByRow($sortArray, $sortIndex, $sortOrder);
63
    }
64
65
    /**
66
     * SORTBY
67
     * The SORTBY function sorts the contents of a range or array based on the values in a corresponding range or array.
68
     * The returned array is the same shape as the provided array argument.
69
     * Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
70
     *
71
     * @param mixed $sortArray The range of cells being sorted
72
     * @param mixed $args
73
     *              At least one additional argument must be provided, The vector or range to sort on
74
     *              After that, arguments are passed as pairs:
75
     *                    sort order: ascending or descending
76
     *                         Ascending = 1 (self::ORDER_ASCENDING)
77
     *                         Descending = -1 (self::ORDER_DESCENDING)
78
     *                    additional arrays or ranges for multi-level sorting
79
     *
80
     * @return mixed The sorted values from the sort range
81
     */
82 13
    public static function sortBy($sortArray, ...$args)
83
    {
84 13
        if (!is_array($sortArray)) {
85
            // Scalars are always returned "as is"
86 1
            return $sortArray;
87
        }
88
89 12
        $sortArray = self::enumerateArrayKeys($sortArray);
90
91 12
        $lookupArraySize = count($sortArray);
92 12
        $argumentCount = count($args);
93
94
        try {
95 12
            $sortBy = $sortOrder = [];
96 12
            for ($i = 0; $i < $argumentCount; $i += 2) {
97 12
                $sortBy[] = self::validateSortVector($args[$i], $lookupArraySize);
98 10
                $sortOrder[] = self::validateSortOrder($args[$i + 1] ?? self::ORDER_ASCENDING);
99
            }
100 6
        } catch (Exception $e) {
101 6
            return $e->getMessage();
102
        }
103
104 6
        return self::processSortBy($sortArray, $sortBy, $sortOrder);
105
    }
106
107 33
    private static function enumerateArrayKeys(array $sortArray): array
108
    {
109 33
        array_walk(
110 33
            $sortArray,
111 33
            function (&$columns): void {
112 33
                if (is_array($columns)) {
113 33
                    $columns = array_values($columns);
114
                }
115 33
            }
116 33
        );
117
118 33
        return array_values($sortArray);
119
    }
120
121
    /**
122
     * @param mixed $sortIndex
123
     * @param mixed $sortOrder
124
     */
125 19
    private static function validateScalarArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void
126
    {
127 19
        if (is_array($sortIndex) || is_array($sortOrder)) {
128
            throw new Exception(ExcelError::VALUE());
129
        }
130
131 19
        $sortIndex = self::validatePositiveInt($sortIndex, false);
132
133 15
        if ($sortIndex > $sortArraySize) {
134 2
            throw new Exception(ExcelError::VALUE());
135
        }
136
137 14
        $sortOrder = self::validateSortOrder($sortOrder);
138
    }
139
140
    /**
141
     * @param mixed $sortVector
142
     */
143 12
    private static function validateSortVector($sortVector, int $sortArraySize): array
144
    {
145 12
        if (!is_array($sortVector)) {
146 1
            throw new Exception(ExcelError::VALUE());
147
        }
148
149
        // It doesn't matter if it's a row or a column vectors, it works either way
150 11
        $sortVector = Functions::flattenArray($sortVector);
151 11
        if (count($sortVector) !== $sortArraySize) {
152 1
            throw new Exception(ExcelError::VALUE());
153
        }
154
155 10
        return $sortVector;
156
    }
157
158
    /**
159
     * @param mixed $sortOrder
160
     */
161 24
    private static function validateSortOrder($sortOrder): int
162
    {
163 24
        $sortOrder = self::validateInt($sortOrder);
164 22
        if (($sortOrder == self::ORDER_ASCENDING || $sortOrder === self::ORDER_DESCENDING) === false) {
165 7
            throw new Exception(ExcelError::VALUE());
166
        }
167
168 16
        return $sortOrder;
169
    }
170
171
    /**
172
     * @param array $sortIndex
173
     * @param mixed $sortOrder
174
     */
175 21
    private static function validateArrayArgumentsForSort(&$sortIndex, &$sortOrder, int $sortArraySize): void
176
    {
177
        // It doesn't matter if they're row or column vectors, it works either way
178 21
        $sortIndex = Functions::flattenArray($sortIndex);
179 21
        $sortOrder = Functions::flattenArray($sortOrder);
180
181
        if (
182 21
            count($sortOrder) === 0 || count($sortOrder) > $sortArraySize ||
183 21
            (count($sortOrder) > count($sortIndex))
184
        ) {
185 2
            throw new Exception(ExcelError::VALUE());
186
        }
187
188 19
        if (count($sortIndex) > count($sortOrder)) {
189
            // If $sortOrder has fewer elements than $sortIndex, then the last order element is repeated.
190 5
            $sortOrder = array_merge(
191 5
                $sortOrder,
192 5
                array_fill(0, count($sortIndex) - count($sortOrder), array_pop($sortOrder))
193 5
            );
194
        }
195
196 19
        foreach ($sortIndex as $key => &$value) {
197 19
            self::validateScalarArgumentsForSort($value, $sortOrder[$key], $sortArraySize);
198
        }
199
    }
200
201 14
    private static function prepareSortVectorValues(array $sortVector): array
202
    {
203
        // Strings should be sorted case-insensitive; with booleans converted to locale-strings
204 14
        return array_map(
205 14
            function ($value) {
206 14
                if (is_bool($value)) {
207
                    return ($value) ? Calculation::getTRUE() : Calculation::getFALSE();
208 14
                } elseif (is_string($value)) {
209 8
                    return StringHelper::strToLower($value);
210
                }
211
212 9
                return $value;
213 14
            },
214 14
            $sortVector
215 14
        );
216
    }
217
218
    /**
219
     * @param array[] $sortIndex
220
     * @param int[] $sortOrder
221
     */
222 6
    private static function processSortBy(array $sortArray, array $sortIndex, $sortOrder): array
223
    {
224 6
        $sortArguments = [];
225 6
        $sortData = [];
226 6
        foreach ($sortIndex as $index => $sortValues) {
227 6
            $sortData[] = $sortValues;
228 6
            $sortArguments[] = self::prepareSortVectorValues($sortValues);
229 6
            $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
230
        }
231 6
232
        $sortVector = self::executeVectorSortQuery($sortData, $sortArguments);
233 6
234
        return self::sortLookupArrayFromVector($sortArray, $sortVector);
235 6
    }
236
237
    /**
238
     * @param int[] $sortIndex
239
     * @param int[] $sortOrder
240
     */
241
    private static function sortByRow(array $sortArray, array $sortIndex, array $sortOrder): array
242 8
    {
243
        $sortVector = self::buildVectorForSort($sortArray, $sortIndex, $sortOrder);
244 8
245
        return self::sortLookupArrayFromVector($sortArray, $sortVector);
246 8
    }
247
248
    /**
249
     * @param int[] $sortIndex
250
     * @param int[] $sortOrder
251
     */
252
    private static function sortByColumn(array $sortArray, array $sortIndex, array $sortOrder): array
253 2
    {
254
        $sortArray = Matrix::transpose($sortArray);
255 2
        $result = self::sortByRow($sortArray, $sortIndex, $sortOrder);
256 2
257
        return Matrix::transpose($result);
258 2
    }
259
260
    /**
261
     * @param int[] $sortIndex
262
     * @param int[] $sortOrder
263
     */
264
    private static function buildVectorForSort(array $sortArray, array $sortIndex, array $sortOrder): array
265 8
    {
266
        $sortArguments = [];
267 8
        $sortData = [];
268 8
        foreach ($sortIndex as $index => $sortIndexValue) {
269 8
            $sortValues = array_column($sortArray, $sortIndexValue - 1);
270 8
            $sortData[] = $sortValues;
271 8
            $sortArguments[] = self::prepareSortVectorValues($sortValues);
272 8
            $sortArguments[] = $sortOrder[$index] === self::ORDER_ASCENDING ? SORT_ASC : SORT_DESC;
273 8
        }
274
275 8
        $sortData = self::executeVectorSortQuery($sortData, $sortArguments);
276
277 8
        return $sortData;
278
    }
279 8
280
    private static function executeVectorSortQuery(array $sortData, array $sortArguments): array
281
    {
282 14
        $sortData = Matrix::transpose($sortData);
283
284 14
        // We need to set an index that can be retained, as array_multisort doesn't maintain numeric keys.
285
        $sortDataIndexed = [];
286
        foreach ($sortData as $key => $value) {
287 14
            $sortDataIndexed[Coordinate::stringFromColumnIndex($key + 1)] = $value;
288 14
        }
289 14
        unset($sortData);
290
291 14
        $sortArguments[] = &$sortDataIndexed;
292
293 14
        array_multisort(...$sortArguments);
294
295 14
        // After the sort, we restore the numeric keys that will now be in the correct, sorted order
296
        $sortedData = [];
297
        foreach (array_keys($sortDataIndexed) as $key) {
298 14
            $sortedData[] = Coordinate::columnIndexFromString($key) - 1;
299 14
        }
300 14
301
        return $sortedData;
302
    }
303 14
304
    private static function sortLookupArrayFromVector(array $sortArray, array $sortVector): array
305
    {
306 14
        // Building a new array in the correct (sorted) order works; but may be memory heavy for larger arrays
307
        $sortedArray = [];
308
        foreach ($sortVector as $index) {
309 14
            $sortedArray[] = $sortArray[$index];
310 14
        }
311 14
312
        return $sortedArray;
313
314 14
//        uksort(
315
//            $lookupArray,
316
//            function (int $a, int $b) use (array $sortVector) {
317
//                return $sortVector[$a] <=> $sortVector[$b];
318
//            }
319
//        );
320
//
321
//        return $lookupArray;
322
    }
323
}
324