1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/** |
4
|
|
|
* OrderedEnumerable class. |
5
|
|
|
* @author Alexander Prokhorov |
6
|
|
|
* @license Simplified BSD |
7
|
|
|
* @link https://github.com/Athari/YaLinqo YaLinqo on GitHub |
8
|
|
|
*/ |
9
|
|
|
|
10
|
|
|
namespace YaLinqo; |
11
|
|
|
|
12
|
|
|
/** |
13
|
|
|
* Subclass of Enumerable supporting ordering by multiple conditions. |
14
|
|
|
* @package YaLinqo |
15
|
|
|
*/ |
16
|
|
|
class OrderedEnumerable extends Enumerable |
17
|
|
|
{ |
18
|
|
|
/** Source sequence. @var Enumerable */ |
19
|
|
|
private $source; |
20
|
|
|
/** Parent ordered sequence. @var \YaLinqo\OrderedEnumerable */ |
21
|
|
|
private $parent; |
22
|
|
|
/** Sort order for array_multisort: SORT_DESC or SORT_ASC. @var int|bool */ |
23
|
|
|
private $sortOrder; |
24
|
|
|
/** Sort flags for array_multisort. @var int */ |
25
|
|
|
private $sortFlags; |
26
|
|
|
/** Whether comparer result needs to be negated (used in usort). @var bool */ |
27
|
|
|
private $isReversed; |
28
|
|
|
/** Key selector. @var callable {(v, k) ==> key} */ |
29
|
|
|
private $keySelector; |
30
|
|
|
/** Comprarer function. @var callable {(a, b) ==> diff} */ |
31
|
|
|
private $comparer; |
32
|
|
|
|
33
|
|
|
/** |
34
|
|
|
* @internal |
35
|
|
|
* @param Enumerable $source |
36
|
|
|
* @param int|bool $sortOrder A direction in which to order the elements: false or SORT_DESC for ascending (by increasing value), true or SORT_ASC for descending (by decreasing value). |
37
|
|
|
* @param int $sortFlags Sort flags for array_multisort. |
38
|
|
|
* @param bool $isReversed Whether comparer result needs to be negated (used in usort). |
39
|
|
|
* @param callable $keySelector {(v, k) ==> key} A function to extract a key from an element. |
40
|
|
|
* @param callable $comparer {(a, b) ==> diff} Difference between a and b: <0 if a<b; 0 if a==b; >0 if a>b |
41
|
|
|
* @param \YaLinqo\OrderedEnumerable $parent |
42
|
|
|
*/ |
43
|
|
|
public function __construct($source, $sortOrder, $sortFlags, $isReversed, $keySelector, $comparer, $parent = null) |
44
|
|
|
{ |
45
|
|
|
$this->source = $source; |
46
|
|
|
$this->sortOrder = $sortOrder; |
47
|
|
|
$this->sortFlags = $sortFlags; |
48
|
|
|
$this->isReversed = $isReversed; |
49
|
|
|
$this->keySelector = $keySelector; |
50
|
|
|
$this->comparer = $comparer; |
51
|
|
|
$this->parent = $parent; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
private function getSingleComparer() |
55
|
|
|
{ |
56
|
|
|
$comparer = $this->comparer; |
57
|
|
|
if ($this->isReversed) |
58
|
|
|
$comparer = function($a, $b) use ($comparer) { return -$comparer($a, $b); }; |
59
|
|
|
return $comparer; |
60
|
|
|
} |
61
|
|
|
|
62
|
|
|
/** |
63
|
|
|
* <p><b>Syntax</b>: thenByDir (false|true [, {{(v, k) ==> key} [, {{(a, b) ==> diff}]]) |
64
|
|
|
* <p>Performs a subsequent ordering of elements in a sequence in a particular direction (ascending, descending) according to a key. |
65
|
|
|
* <p>Three methods are defined to extend the type OrderedEnumerable, which is the return type of this method. These three methods, namely {@link thenBy}, {@link thenByDescending} and {@link thenByDir}, enable you to specify additional sort criteria to sort a sequence. These methods also return an OrderedEnumerable, which means any number of consecutive calls to thenBy, thenByDescending or thenByDir can be made. |
66
|
|
|
* <p>Because OrderedEnumerable inherits from {@link Enumerable}, you can call {@link Enumerable::orderBy orderBy}, {@link Enumerable::orderByDescending orderByDescending} or {@link Enumerable::orderByDir orderByDir} on the results of a call to orderBy, orderByDescending, orderByDir, thenBy, thenByDescending or thenByDir. Doing this introduces a new primary ordering that ignores the previously established ordering. |
67
|
|
|
* <p>This method performs an unstable sort; that is, if the keys of two elements are equal, the order of the elements is not preserved. In contrast, a stable sort preserves the order of elements that have the same key. Internally, {@link usort} is used. |
68
|
|
|
* @param int|bool $sortOrder A direction in which to order the elements: false or SORT_DESC for ascending (by increasing value), true or SORT_ASC for descending (by decreasing value). |
69
|
|
|
* @param callable|null $keySelector {(v, k) ==> key} A function to extract a key from an element. Default: value. |
70
|
|
|
* @param callable|int|null $comparer {(a, b) ==> diff} Difference between a and b: <0 if a<b; 0 if a==b; >0 if a>b. Can also be a combination of SORT_ flags. |
71
|
|
|
* @return \YaLinqo\OrderedEnumerable |
72
|
|
|
*/ |
73
|
|
|
public function thenByDir($sortOrder, $keySelector = null, $comparer = null): OrderedEnumerable |
74
|
|
|
{ |
75
|
|
|
$sortFlags = Utils::lambdaToSortFlagsAndOrder($comparer, $sortOrder); |
76
|
|
|
$keySelector = Utils::createLambda($keySelector, 'v,k', Functions::$value); |
77
|
|
|
$isReversed = $sortOrder == SORT_DESC; |
78
|
|
|
$comparer = Utils::createComparer($comparer, $sortOrder, $isReversed); |
|
|
|
|
79
|
|
|
return new self($this->source, $sortOrder, $sortFlags, $isReversed, $keySelector, $comparer, $this); |
|
|
|
|
80
|
|
|
} |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* <p><b>Syntax</b>: thenBy ([{{(v, k) ==> key} [, {{(a, b) ==> diff}]]) |
84
|
|
|
* <p>Performs a subsequent ordering of the elements in a sequence in ascending order according to a key. |
85
|
|
|
* <p>Three methods are defined to extend the type OrderedEnumerable, which is the return type of this method. These three methods, namely {@link thenBy}, {@link thenByDescending} and {@link thenByDir}, enable you to specify additional sort criteria to sort a sequence. These methods also return an OrderedEnumerable, which means any number of consecutive calls to thenBy, thenByDescending or thenByDir can be made. |
86
|
|
|
* <p>Because OrderedEnumerable inherits from {@link Enumerable}, you can call {@link Enumerable::orderBy orderBy}, {@link Enumerable::orderByDescending orderByDescending} or {@link Enumerable::orderByDir orderByDir} on the results of a call to orderBy, orderByDescending, orderByDir, thenBy, thenByDescending or thenByDir. Doing this introduces a new primary ordering that ignores the previously established ordering. |
87
|
|
|
* <p>This method performs an unstable sort; that is, if the keys of two elements are equal, the order of the elements is not preserved. In contrast, a stable sort preserves the order of elements that have the same key. Internally, {@link usort} is used. |
88
|
|
|
* @param callable|null $keySelector {(v, k) ==> key} A function to extract a key from an element. Default: value. |
89
|
|
|
* @param callable|int|null $comparer {(a, b) ==> diff} Difference between a and b: <0 if a<b; 0 if a==b; >0 if a>b. Can also be a combination of SORT_ flags. |
90
|
|
|
* @return \YaLinqo\OrderedEnumerable |
91
|
|
|
*/ |
92
|
|
|
public function thenBy($keySelector = null, $comparer = null): OrderedEnumerable |
93
|
|
|
{ |
94
|
|
|
return $this->thenByDir(false, $keySelector, $comparer); |
95
|
|
|
} |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* <p><b>Syntax</b>: thenByDescending ([{{(v, k) ==> key} [, {{(a, b) ==> diff}]]) |
99
|
|
|
* <p>Performs a subsequent ordering of the elements in a sequence in descending order according to a key. |
100
|
|
|
* <p>Three methods are defined to extend the type OrderedEnumerable, which is the return type of this method. These three methods, namely {@link thenBy}, {@link thenByDescending} and {@link thenByDir}, enable you to specify additional sort criteria to sort a sequence. These methods also return an OrderedEnumerable, which means any number of consecutive calls to thenBy, thenByDescending or thenByDir can be made. |
101
|
|
|
* <p>Because OrderedEnumerable inherits from {@link Enumerable}, you can call {@link Enumerable::orderBy orderBy}, {@link Enumerable::orderByDescending orderByDescending} or {@link Enumerable::orderByDir orderByDir} on the results of a call to orderBy, orderByDescending, orderByDir, thenBy, thenByDescending or thenByDir. Doing this introduces a new primary ordering that ignores the previously established ordering. |
102
|
|
|
* <p>This method performs an unstable sort; that is, if the keys of two elements are equal, the order of the elements is not preserved. In contrast, a stable sort preserves the order of elements that have the same key. Internally, {@link usort} is used. |
103
|
|
|
* @param callable|null $keySelector {(v, k) ==> key} A function to extract a key from an element. Default: value. |
104
|
|
|
* @param callable|int|null $comparer {(a, b) ==> diff} Difference between a and b: <0 if a<b; 0 if a==b; >0 if a>b. Can also be a combination of SORT_ flags. |
105
|
|
|
* @return \YaLinqo\OrderedEnumerable |
106
|
|
|
*/ |
107
|
|
|
public function thenByDescending($keySelector = null, $comparer = null): OrderedEnumerable |
108
|
|
|
{ |
109
|
|
|
return $this->thenByDir(true, $keySelector, $comparer); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** {@inheritdoc} */ |
113
|
|
|
public function getIterator(): \Traversable |
114
|
|
|
{ |
115
|
|
|
$canMultisort = $this->sortFlags !== null; |
116
|
|
|
$array = $this->source->tryGetArrayCopy(); |
117
|
|
|
|
118
|
|
|
$it = $this->trySortBySingleField($array, $canMultisort); |
119
|
|
|
if ($it !== null) |
120
|
|
|
return $it; |
121
|
|
|
|
122
|
|
|
return $this->sortByMultipleFields($array, $canMultisort); |
123
|
|
|
} |
124
|
|
|
|
125
|
|
|
private function trySortBySingleField($array, bool $canMultisort) |
126
|
|
|
{ |
127
|
|
|
if ($this->parent !== null || $array === null) { |
128
|
|
|
return null; |
129
|
|
|
} |
130
|
|
|
elseif ($this->keySelector === Functions::$value) { |
131
|
|
|
if (!$canMultisort) |
132
|
|
|
uasort($array, $this->getSingleComparer()); |
133
|
|
|
elseif ($this->sortOrder == SORT_ASC) |
134
|
|
|
asort($array, $this->sortFlags); |
135
|
|
|
else |
136
|
|
|
arsort($array, $this->sortFlags); |
137
|
|
|
} |
138
|
|
|
elseif ($this->keySelector === Functions::$key) { |
139
|
|
|
if ($canMultisort) |
140
|
|
|
uksort($array, $this->getSingleComparer()); |
141
|
|
|
elseif ($this->sortOrder == SORT_ASC) |
142
|
|
|
ksort($array, $this->sortFlags); |
143
|
|
|
else |
144
|
|
|
krsort($array, $this->sortFlags); |
145
|
|
|
} |
146
|
|
|
else { |
147
|
|
|
return null; |
148
|
|
|
} |
149
|
|
|
return new \ArrayIterator($array); |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
private function sortByMultipleFields($array, bool $canMultisort) |
153
|
|
|
{ |
154
|
|
|
$orders = []; |
155
|
|
|
for ($order = $this; $order !== null; $order = $order->parent) { |
156
|
|
|
$orders[] = $order; |
157
|
|
|
if ($order->sortFlags === null) |
158
|
|
|
$canMultisort = false; |
159
|
|
|
} |
160
|
|
|
$orders = array_reverse($orders); |
161
|
|
|
|
162
|
|
|
$it = $this->trySortArrayWithMultisort($array, $orders, $canMultisort); |
163
|
|
|
if ($it !== null) |
164
|
|
|
return $it; |
165
|
|
|
|
166
|
|
|
return $this->sortIterator($orders, $canMultisort); |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
private function sortIterator(array $orders, bool $canMultisort) |
170
|
|
|
{ |
171
|
|
|
$enum = []; |
172
|
|
|
if ($canMultisort) |
173
|
|
|
$this->sortIteratorWithMultisort($enum, $orders); |
174
|
|
|
else |
175
|
|
|
$this->sortIteratorWithUsort($enum, $orders); |
176
|
|
|
|
177
|
|
|
foreach ($enum as $pair) |
178
|
|
|
yield $pair[0] => $pair[1]; |
179
|
|
|
} |
180
|
|
|
|
181
|
|
|
private function trySortArrayWithMultisort($array, array $orders, bool $canMultisort) |
182
|
|
|
{ |
183
|
|
|
/** @var $order OrderedEnumerable */ |
184
|
|
|
if ($array === null || !$canMultisort) |
185
|
|
|
return null; |
186
|
|
|
|
187
|
|
|
$args = []; |
188
|
|
|
foreach ($orders as $order) { |
189
|
|
|
$column = []; |
190
|
|
|
foreach ($array as $k => $v) { |
191
|
|
|
$keySelector = $order->keySelector; |
192
|
|
|
$column[$k] = $keySelector($v, $k); |
193
|
|
|
} |
194
|
|
|
$args[] = $column; |
195
|
|
|
$args[] = $order->sortOrder; |
196
|
|
|
$args[] = $order->sortFlags; |
197
|
|
|
} |
198
|
|
|
$args[] = &$array; |
199
|
|
|
|
200
|
|
|
call_user_func_array('array_multisort', $args); |
201
|
|
|
|
202
|
|
|
return new \ArrayIterator($array); |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
private function sortIteratorWithMultisort(&$enum, array $orders) |
206
|
|
|
{ |
207
|
|
|
/** @var $order OrderedEnumerable */ |
208
|
|
|
foreach ($this->source as $k => $v) |
209
|
|
|
$enum[] = [ $k, $v ]; |
210
|
|
|
|
211
|
|
|
$args = []; |
212
|
|
|
foreach ($orders as $order) { |
213
|
|
|
$column = []; |
214
|
|
|
foreach ($enum as $k => $pair) { |
215
|
|
|
$keySelector = $order->keySelector; |
216
|
|
|
$column[$k] = $keySelector($pair[1], $pair[0]); |
217
|
|
|
} |
218
|
|
|
$args[] = $column; |
219
|
|
|
$args[] = $order->sortOrder; |
220
|
|
|
$args[] = $order->sortFlags; |
221
|
|
|
} |
222
|
|
|
$args[] = &$enum; |
223
|
|
|
|
224
|
|
|
call_user_func_array('array_multisort', $args); |
225
|
|
|
} |
226
|
|
|
|
227
|
|
|
private function sortIteratorWithUsort(&$enum, array $orders) |
228
|
|
|
{ |
229
|
|
|
/** @var $order OrderedEnumerable */ |
230
|
|
|
foreach ($this->source as $k => $v) { |
231
|
|
|
$element = [ $k, $v ]; |
232
|
|
|
foreach ($orders as $order) { |
233
|
|
|
$keySelector = $order->keySelector; |
234
|
|
|
$element[] = $keySelector($v, $k); |
235
|
|
|
} |
236
|
|
|
$enum[] = $element; |
237
|
|
|
} |
238
|
|
|
|
239
|
|
|
usort($enum, function($a, $b) use ($orders) { |
240
|
|
|
/** @var $order OrderedEnumerable */ |
241
|
|
|
$count = count($orders); |
242
|
|
|
for ($i = 0; $i < $count; $i++) { |
243
|
|
|
$order = $orders[$i]; |
244
|
|
|
$comparer = $order->comparer; |
245
|
|
|
$diff = $comparer($a[$i + 2], $b[$i + 2]); |
246
|
|
|
if ($diff != 0) |
247
|
|
|
return $order->isReversed ? -$diff : $diff; |
248
|
|
|
} |
249
|
|
|
return 0; |
250
|
|
|
}); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.