Passed
Pull Request — master (#849)
by
unknown
14:18
created

TPriorityList::removeAtIndexInPriority()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 22
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6.0208

Importance

Changes 0
Metric Value
cc 6
eloc 12
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 22
rs 9.2222
ccs 11
cts 12
cp 0.9167
crap 6.0208
1
<?php
2
/**
3
 * TPriorityList class
4
 *
5
 * @author Brad Anderson <[email protected]>
6
 * @link https://github.com/pradosoft/prado
7
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
8
 */
9
10
namespace Prado\Collections;
11
12
use Prado\Exceptions\TInvalidDataTypeException;
13
use Prado\Exceptions\TInvalidDataValueException;
14
use Prado\Exceptions\TInvalidOperationException;
15
use Prado\TPropertyValue;
16
17
/**
18
 * TPriorityList class
19
 *
20
 * TPriorityList implements a priority ordered list collection class.  It allows you to specify
21
 * any numeric for priorities down to a specific precision.  The lower the numeric, the high the priority of the item in the
22
 * list.  Thus -10 has a higher priority than -5, 0, 10 (the default), 18, 10005, etc.  Per {@link round}, precision may be negative and
23
 * thus rounding can go by 10, 100, 1000, etc, instead of just .1, .01, .001, etc. The default precision allows for 8 decimal
24
 * places. There is also a default priority of 10, if no different default priority is specified or no item specific priority is indicated.
25
 * If you replace TList with this class it will  work exactly the same with items inserted set to the default priority, until you start
26
 * using different priorities than the default priority.
27
 *
28
 * As you access the PHP array features of this class, it flattens and caches the results.  If at all possible, this
29
 * will keep the cache fresh even when manipulated.  If this is not possible the cache is cleared.
30
 * When an array of items are needed and the cache is outdated, the cache is recreated from the items and their priorities
31
 *
32
 * You can access, append, insert, remove an item by using
33
 * {@link itemAt}, {@link add}, {@link insertAt}, and {@link remove}.
34
 * To get the number of the items in the list, use {@link getCount}.
35
 * TPriorityList can also be used like a regular array as follows,
36
 * <code>
37
 * $list[]=$item;  // append with the default priority.  It may not be the last item if other items in the list are prioritized after the default priority
38
 * $list[$index]=$item; // $index must be between 0 and $list->Count-1.  This sets the element regardless of priority.  Priority stays the same.
39
 * $list[$index]=$item; // $index is $list->Count.  This appends the item to the end of the list with the same priority as the last item in the list.
40
 * unset($list[$index]); // remove the item at $index
41
 * if(isset($list[$index])) // if the list has an item at $index
42
 * foreach($list as $index=>$item) // traverse each item in the list in proper priority order and add/insert order
43
 * $n=count($list); // returns the number of items in the list
44
 * </code>
45
 *
46
 * To extend TPriorityList for doing your own operations with each addition or removal,
47
 * override {@link insertAtIndexInPriority()} and {@link removeAtIndexInPriority()} and then call the parent.
48
 *
49
 * @author Brad Anderson <[email protected]>
50
 * @since 3.2a
51
 */
52
class TPriorityList extends TList
53
{
54
	/**
55
	 * @var array internal data storage
56
	 */
57
	private $_d = [];
58
	/**
59
	 * @var bool indicates if the _d is currently ordered.
60
	 */
61
	private $_o = false;
62
	/**
63
	 * @var null|array cached flattened internal data storage
64
	 */
65
	private $_fd;
66
	/**
67
	 * @var int number of items contain within the list
68
	 */
69
	private $_c = 0;
70
	/**
71
	 * @var numeric the default priority of items without specified priorities
0 ignored issues
show
Bug introduced by
The type Prado\Collections\numeric was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
72
	 */
73
	private $_dp = 10;
74
	/**
75
	 * @var int the precision of the numeric priorities within this priority list.
76
	 */
77
	private $_p = 8;
78
79
	/**
80
	 * Constructor.
81
	 * Initializes the list with an array or an iterable object.
82
	 * @param null|array|\Iterator $data the initial data. Default is null, meaning no initial data.
83
	 * @param bool $readOnly whether the list is read-only
84
	 * @param numeric $defaultPriority the default priority of items without specified priorities.
85
	 * @param int $precision the precision of the numeric priorities
86
	 * @throws TInvalidDataTypeException If data is not null and is neither an array nor an iterator.
87
	 */
88
	public function __construct($data = null, $readOnly = false, $defaultPriority = 10, $precision = 8)
89
	{
90 90
		parent::__construct();
91
		if ($data !== null) {
92 90
			$this->copyFrom($data);
93 90
		}
94 25
		$this->setReadOnly($readOnly);
95
		$this->setPrecision($precision);
96 90
		$this->setDefaultPriority($defaultPriority);
0 ignored issues
show
Bug introduced by
It seems like $defaultPriority can also be of type integer; however, parameter $value of Prado\Collections\TPrior...t::setDefaultPriority() does only seem to accept Prado\Collections\numeric, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

96
		$this->setDefaultPriority(/** @scrutinizer ignore-type */ $defaultPriority);
Loading history...
97 90
	}
98 90
99 90
	/**
100
	 * Returns the number of items in the list.
101
	 * This method is required by \Countable interface.
102
	 * @return int number of items in the list.
103
	 */
104
	public function count(): int
105
	{
106 3
		return $this->getCount();
107
	}
108 3
109
	/**
110
	 * Returns the total number of items in the list
111
	 * @return int the number of items in the list
112
	 */
113
	public function getCount()
114
	{
115 133
		return $this->_c;
116
	}
117 133
118
	/**
119
	 * Gets the number of items at a priority within the list
120
	 * @param null|numeric $priority optional priority at which to count items.  if no parameter, it will be set to the default {@link getDefaultPriority}
121
	 * @return false|int the number of items in the list at the specified priority
122
	 */
123
	public function getPriorityCount($priority = null)
124
	{
125 2
		$priority = $this->ensurePriority($priority);
126
		if (!isset($this->_d[$priority]) || !is_array($this->_d[$priority])) {
127 2
			return false;
128 2
		}
129
		return count($this->_d[$priority]);
130 2
	}
131
132 2
	/**
133
	 * @return numeric gets the default priority of inserted items without a specified priority
134
	 */
135 2
	public function getDefaultPriority()
136
	{
137
		return $this->_dp;
138
	}
139
140
	/**
141 116
	 * This must be called internally or when instantiated.
142
	 * @param numeric $value sets the default priority of inserted items without a specified priority
143 116
	 */
144
	protected function setDefaultPriority($value)
145
	{
146
		$this->_dp = (string) round(TPropertyValue::ensureFloat($value), $this->_p);
0 ignored issues
show
Documentation Bug introduced by
It seems like (string)round(Prado\TPro...oat($value), $this->_p) of type string is incompatible with the declared type Prado\Collections\numeric of property $_dp.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
147
	}
148
149
	/**
150 90
	 * @return int The precision of numeric priorities, defaults to 8
151
	 */
152 90
	public function getPrecision()
153 90
	{
154
		return $this->_p;
155
	}
156
157
	/**
158 1
	 * This must be called internally or when instantiated.
159
	 * @param int $value The precision of numeric priorities.
160 1
	 */
161
	protected function setPrecision($value)
162
	{
163
		$this->_p = TPropertyValue::ensureInteger($value);
164
	}
165
166
	/**
167 90
	 * Taken an input Priority and ensures its value.
168
	 * Sets the default $priority when none is set,
169 90
	 * then rounds to the proper precision and makes
170 90
	 * into a string.
171
	 * @param mixed $priority
172
	 * @return string the priority in string format
173
	 */
174
	protected function ensurePriority($priority): string
175
	{
176
		if ($priority === null || !is_numeric($priority)) {
177 2
			$priority = $this->getDefaultPriority();
178
		}
179 2
		return (string) round(TPropertyValue::ensureFloat($priority), $this->_p);
180
	}
181
182
	/**
183
	 * Returns an iterator for traversing the items in the list.
184
	 * This method is required by the interface \IteratorAggregate.
185
	 * @return \Iterator an iterator for traversing the items in the list.
186 16
	 */
187
	#[\ReturnTypeWillChange]
188 16
	public function getIterator()
189 16
	{
190
		return new \ArrayIterator($this->flattenPriorities());
191
	}
192
193
	/**
194
	 * This returns a list of the priorities within this list, ordered lowest to highest.
195
	 * @return array the array of priority numerics in decreasing priority order
196 121
	 */
197
	public function getPriorities()
198 121
	{
199 112
		$this->sortPriorities();
200 112
		return array_keys($this->_d);
201
	}
202 121
203
204
	/**
205
	 * This orders the priority list internally.
206
	 */
207
	protected function sortPriorities()
208 93
	{
209
		if (!$this->_o) {
210 93
			ksort($this->_d, SORT_NUMERIC);
211 68
			$this->_o = true;
212
		}
213
	}
214 59
215 59
	/**
216 59
	 * This flattens the priority list into a flat array [0,...,n-1]
217 56
	 * @return array array of items in the list in priority and index order
218
	 */
219 59
	protected function flattenPriorities()
220
	{
221
		if (is_array($this->_fd)) {
222
			return $this->_fd;
223
		}
224
225
		$this->sortPriorities();
226
		$this->_fd = [];
227
		foreach ($this->_d as $priority => $itemsatpriority) {
228
			$this->_fd = array_merge($this->_fd, $itemsatpriority);
229
		}
230 8
		return $this->_fd;
231
	}
232 8
233 8
234 8
	/**
235
	 * Returns the item at the index of a flattened priority list.
236 2
	 * {@link offsetGet} calls this method.
237
	 * @param int $index the index of the item to get
238
	 * @throws TInvalidDataValueException Issued when the index is invalid
239
	 * @return mixed the element at the offset
240
	 */
241
	public function itemAt($index)
242
	{
243
		if ($index >= 0 && $index < $this->getCount()) {
244
			$arr = $this->flattenPriorities();
245 16
			return $arr[$index];
246
		} else {
247 16
			throw new TInvalidDataValueException('list_index_invalid', $index);
248 1
		}
249
	}
250 16
251
	/**
252 16
	 * Gets all the items at a specific priority.
253
	 * @param null|numeric $priority priority of the items to get.  Defaults to null, filled in with the default priority, if left blank.
254
	 * @return array all items at priority in index order, null if there are no items at that priority
255
	 */
256
	public function itemsAtPriority($priority = null)
257
	{
258
		$priority = $this->ensurePriority($priority);
259
		return $this->_d[$priority] ?? null;
260
	}
261 1
262
	/**
263 1
	 * Returns the item at an index within a priority
264 1
	 * @param int $index the index into the list of items at priority
265
	 * @param numeric $priority the priority which to index.  no parameter or null will result in the default priority
266 1
	 * @return mixed the element at the offset, false if no element is found at the offset
267
	 */
268 1
	public function itemAtIndexInPriority($index, $priority = null)
269 1
	{
270
		$priority = $this->ensurePriority($priority);
271
		return !isset($this->_d[$priority]) ? false : (
272
			$this->_d[$priority][$index] ?? false
273
		);
274
	}
275
276
	/**
277
	 * Appends an item into the list at the end of the specified priority.  The position of the added item may
278
	 * not be at the end of the list.
279
	 * @param mixed $item item to add into the list at priority
280
	 * @param null|numeric $priority priority blank or null for the default priority
281 116
	 * @throws TInvalidOperationException if the map is read-only
282
	 * @return int the index within the flattened array
283 116
	 */
284 1
	public function add($item, $priority = null)
285
	{
286
		if ($this->getReadOnly()) {
287 116
			throw new TInvalidOperationException('list_readonly', get_class($this));
288
		}
289
290
		return $this->insertAtIndexInPriority($item, false, $priority, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->insertAtIn...false, $priority, true) could also return false which is incompatible with the documented return type integer. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
291
	}
292
293
	/**
294
	 * Inserts an item at an index.  It reads the priority of the item at index within the flattened list
295
	 * and then inserts the item at that priority-index.
296
	 * @param int $index the specified position in the flattened list.
297
	 * @param mixed $item new item to add
298 3
	 * @throws TInvalidDataValueException If the index specified exceeds the bound
299
	 * @throws TInvalidOperationException if the list is read-only
300 3
	 */
301 1
	public function insertAt($index, $item)
302
	{
303
		if ($this->getReadOnly()) {
304 2
			throw new TInvalidOperationException('list_readonly', get_class($this));
305 2
		}
306
307
		if (($priority = $this->priorityAt($index, true)) !== false) {
308
			$this->insertAtIndexInPriority($item, $priority[1], $priority[0]);
309 2
		} else {
310
			throw new TInvalidDataValueException('list_index_invalid', $index);
311
		}
312
	}
313
314
	/**
315
	 * Inserts an item at the specified index within a priority.  Override and call this method to
316
	 * insert your own functionality.
317
	 * @param mixed $item item to add within the list.
318
	 * @param false|int $index index within the priority to add the item, defaults to false which appends the item at the priority
319
	 * @param null|numeric $priority priority of the item.  defaults to null, which sets it to the default priority
320
	 * @param bool $preserveCache preserveCache specifies if this is a special quick function or not. This defaults to false.
321 116
	 * @throws TInvalidDataValueException If the index specified exceeds the bound
322
	 * @throws TInvalidOperationException if the list is read-only
323 116
	 */
324 1
	public function insertAtIndexInPriority($item, $index = false, $priority = null, $preserveCache = false)
325
	{
326
		if ($this->getReadOnly()) {
327 116
			throw new TInvalidOperationException('list_readonly', get_class($this));
328 116
		}
329
330 116
		$itemPriority = null;
331
		if (($priority === null || !is_numeric($priority)) && $item instanceof IPriorityItem) {
0 ignored issues
show
introduced by
The condition is_numeric($priority) is always false.
Loading history...
332 116
			$itemPriority = $priority = $item->getPriority();
333 116
		}
334 116
		$priority = $this->ensurePriority($priority);
335 116
		if (($item instanceof IPriorityProperty) && $itemPriority != $priority) {
336 89
			$item->setPriority($priority);
0 ignored issues
show
Bug introduced by
$priority of type string is incompatible with the type Prado\Collections\numeric expected by parameter $value of Prado\Collections\IPriorityProperty::setPriority(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

336
			$item->setPriority(/** @scrutinizer ignore-type */ $priority);
Loading history...
337 89
		}
338
339 65
		if ($preserveCache) {
340
			if ($index === false && isset($this->_d[$priority])) {
341
				$c = count($this->_d[$priority]);
342
				$this->_d[$priority][] = $item;
343 116
			} elseif (isset($this->_d[$priority])) {
344 89
				$c = $index;
345 89
				array_splice($this->_d[$priority], $index, 0, [$item]);
0 ignored issues
show
Bug introduced by
It seems like $index can also be of type false; however, parameter $offset of array_splice() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

345
				array_splice($this->_d[$priority], /** @scrutinizer ignore-type */ $index, 0, [$item]);
Loading history...
346 89
			} else {
347 106
				$c = 0;
348 1
				$this->_o = false;
349 1
				$this->_d[$priority] = [$item];
350
			}
351 106
352 106
			if ($this->_fd !== null) { // if there is a flattened array cache
353 106
				$this->sortPriorities();
354
				foreach ($this->_d as $prioritykey => $items) {
355
					if ($prioritykey >= $priority) {
356 116
						break;
357 116
					} else {
358
						$c += count($items);
359
					}
360 20
				}
361 20
				array_splice($this->_fd, $c, 0, [$item]);
362 2
			}
363 2
		} else {
364 20
			$c = null;
365 18
			if ($index === false && isset($this->_d[$priority])) {
366 18
				$cc = count($this->_d[$priority]);
367
				$this->_d[$priority][] = $item;
368 17
			} elseif (isset($this->_d[$priority])) {
369 17
				$cc = $index;
370 17
				array_splice($this->_d[$priority], $index, 0, [$item]);
371
			} else {
372 20
				$cc = 0;
373 1
				$this->_o = false;
374
				$this->_d[$priority] = [$item];
375 20
			}
376
			if ($this->_fd !== null && count($this->_d) == 1) {
377
				array_splice($this->_fd, $cc, 0, [$item]);
378
			} else {
379 116
				$this->_fd = null;
380
			}
381 116
		}
382
383
		$this->_c++;
384
385
		return $c;
386
	}
387
388
389
	/**
390
	 * Removes an item from the priority list.
391
	 * The list will search for the item.  The first matching item found will be removed from the list.
392
	 * @param mixed $item item the item to be removed.
393
	 * @param null|bool|float $priority priority of item to remove. without this parameter it defaults to false.
394 41
	 * A value of false means any priority. null will be filled in with the default priority.
395
	 * @throws TInvalidDataValueException If the item does not exist
396 41
	 * @return int index within the flattened list at which the item is being removed
397 1
	 */
398
	public function remove($item, $priority = false)
399
	{
400 40
		if ($this->getReadOnly()) {
401 40
			throw new TInvalidOperationException('list_readonly', get_class($this));
402 1
		}
403 1
404
		if (($p = $this->priorityOf($item, true)) !== false) {
405 1
			if ($priority !== false) {
406
				$priority = $this->ensurePriority($priority);
407 1
				if ($p[0] != $priority) {
408 1
					throw new TInvalidDataValueException('list_item_inexistent');
409
				}
410
			}
411 40
			$this->removeAtIndexInPriority($p[1], $p[0]);
412 40
			return $p[2];
413
		} else {
414 2
			throw new TInvalidDataValueException('list_item_inexistent');
415
		}
416
	}
417
418
	/**
419
	 * Removes an item at the specified index in the flattened list.
420
	 * @param int $index index of the item to be removed.
421
	 * @throws TInvalidDataValueException If the index specified exceeds the bound
422
	 * @throws TInvalidOperationException if the list is read-only
423
	 * @return mixed the removed item.
424
	 */
425 5
	public function removeAt($index)
426
	{
427 5
		if ($this->getReadOnly()) {
428 1
			throw new TInvalidOperationException('list_readonly', get_class($this));
429
		}
430
431 4
		if (($priority = $this->priorityAt($index, true)) !== false) {
432 4
			return $this->removeAtIndexInPriority($priority[1], $priority[0]);
433
		}
434
		throw new TInvalidDataValueException('list_index_invalid', $index);
435
	}
436
437
	/**
438
	 * Removes the item at a specific index within a priority.  Override
439
	 * and call this method to insert your own functionality.
440
	 * @param int $index index of item to remove within the priority.
441
	 * @param null|numeric $priority priority of the item to remove, defaults to null, or left blank, it is then set to the default priority
442
	 * @throws TInvalidDataValueException If the item does not exist
443
	 * @return mixed the removed item.
444
	 */
445 51
	public function removeAtIndexInPriority($index, $priority = null)
446
	{
447 51
		if ($this->getReadOnly()) {
448 1
			throw new TInvalidOperationException('list_readonly', get_class($this));
449
		}
450
451 50
		$priority = $this->ensurePriority($priority);
452
		if (!isset($this->_d[$priority]) || $index < 0 || $index >= count($this->_d[$priority])) {
453
			throw new TInvalidDataValueException('list_item_inexistent');
454 50
		}
455
456 50
		// $value is an array of elements removed, only one
457 1
		$value = array_splice($this->_d[$priority], $index, 1);
458
		$value = $value[0];
459
460
		if (!count($this->_d[$priority])) {
461 49
			unset($this->_d[$priority]);
462 49
		}
463
464 49
		$this->_c--;
465 30
		$this->_fd = null;
466
		return $value;
467
	}
468 49
469 49
	/**
470 49
	 * Removes all items in the priority list by calling removeAtIndexInPriority from the last item to the first.
471
	 */
472
	public function clear()
473
	{
474
		if ($this->getReadOnly()) {
475
			throw new TInvalidOperationException('list_readonly', get_class($this));
476 7
		}
477
478 7
		foreach ($this->_d as $priority => $items) {
479 1
			for ($index = count($items) - 1;$index >= 0;$index--) {
480
				$this->removeAtIndexInPriority($index, $priority);
481
			}
482 6
			unset($this->_d[$priority]);
483 6
		}
484 6
	}
485
486 6
	/**
487
	 * @param mixed $item item
488 6
	 * @return bool whether the list contains the item
489
	 */
490
	public function contains($item)
491
	{
492
		return $this->indexOf($item) >= 0;
493
	}
494 2
495
	/**
496 2
	 * @param mixed $item item
497
	 * @return int the index of the item in the flattened list (0 based), -1 if not found.
498
	 */
499
	public function indexOf($item)
500
	{
501
		if (($index = array_search($item, $this->flattenPriorities(), true)) === false) {
502
			return -1;
503 13
		} else {
504
			return $index;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $index also could return the type string which is incompatible with the documented return type integer.
Loading history...
505 13
		}
506 11
	}
507
508 11
	/**
509
	 * Returns the priority of a particular item
510
	 * @param mixed $item the item to look for within the list
511
	 * @param bool $withindex this specifies if the full positional data of the item within the list is returned.
512
	 * 		This defaults to false, if no parameter is provided, so only provides the priority number of the item by default.
513
	 * @return array|false|numeric the priority of the item in the list, false if not found.
514
	 *   if withindex is true, an array is returned of [0 => $priority, 1 => $priorityIndex, 2 => flattenedIndex,
515
	 * 'priority' => $priority, 'index' => $priorityIndex, 'absindex' => flattenedIndex]
516
	 */
517
	public function priorityOf($item, $withindex = false)
518
	{
519
		$this->sortPriorities();
520
521 48
		$absindex = 0;
522
		foreach ($this->_d as $priority => $items) {
523 48
			if (($index = array_search($item, $items, true)) !== false) {
524
				$absindex += $index;
525 48
				return $withindex ? [$priority, $index, $absindex,
526 48
						'priority' => $priority, 'index' => $index, 'absindex' => $absindex] : $priority;
527 48
			} else {
528 46
				$absindex += count($items);
529 46
			}
530 46
		}
531
532 11
		return false;
533
	}
534
535
	/**
536 4
	 * Retutrns the priority of an item at a particular flattened index.
537
	 * @param int $index index of the item within the list
538
	 * @param bool $withindex this specifies if the full positional data of the item within the list is returned.
539
	 * 		This defaults to false, if no parameter is provided, so only provides the priority number of the item by default.
540
	 * @return array|false|numeric the priority of the item in the list, false if not found.
541
	 *   if withindex is true, an array is returned of [0 => $priority, 1 => $priorityIndex, 2 => flattenedIndex,
542
	 * 'priority' => $priority, 'index' => $priorityIndex, 'absindex' => flattenedIndex]
543
	 */
544
	public function priorityAt($index, $withindex = false)
545
	{
546
		if ($index < 0 || $index >= $this->getCount()) {
547
			throw new TInvalidDataValueException('list_index_invalid', $index);
548 12
		}
549
550 12
		$absindex = $index;
551 4
		$this->sortPriorities();
552
		foreach ($this->_d as $priority => $items) {
553
			if ($index >= ($c = count($items))) {
554 12
				$index -= $c;
555 12
			} else {
556 12
				return $withindex ? [$priority, $index, $absindex,
557 12
						'priority' => $priority, 'index' => $index, 'absindex' => $absindex] : $priority;
558 7
			}
559
		}
560 12
		return false;
561 12
	}
562
563
	/**
564
	 * This inserts an item before another item within the list.  It uses the same priority as the
565
	 * found index item and places the new item before it.
566
	 * @param mixed $indexitem the item to index
567
	 * @param mixed $item the item to add before indexitem
568
	 * @throws TInvalidDataValueException If the item does not exist
569
	 * @return int where the item has been inserted in the flattened list
570
	 */
571
	public function insertBefore($indexitem, $item)
572
	{
573
		if ($this->getReadOnly()) {
574
			throw new TInvalidOperationException('list_readonly', get_class($this));
575 3
		}
576
577 3
		if (($priority = $this->priorityOf($indexitem, true)) === false) {
578 1
			throw new TInvalidDataValueException('list_item_inexistent');
579
		}
580
581 2
		$this->insertAtIndexInPriority($item, $priority[1], $priority[0]);
582 1
583
		return $priority[2];
584
	}
585 1
586
	/**
587 1
	 * This inserts an item after another item within the list.  It uses the same priority as the
588
	 * found index item and places the new item after it.
589
	 * @param mixed $indexitem the item to index
590
	 * @param mixed $item the item to add after indexitem
591
	 * @throws TInvalidDataValueException If the item does not exist
592
	 * @return int where the item has been inserted in the flattened list
593
	 */
594
	public function insertAfter($indexitem, $item)
595
	{
596
		if ($this->getReadOnly()) {
597
			throw new TInvalidOperationException('list_readonly', get_class($this));
598 3
		}
599
600 3
		if (($priority = $this->priorityOf($indexitem, true)) === false) {
601 1
			throw new TInvalidDataValueException('list_item_inexistent');
602
		}
603
604 2
		$this->insertAtIndexInPriority($item, $priority[1] + 1, $priority[0]);
605 1
606
		return $priority[2] + 1;
607
	}
608 1
609
	/**
610 1
	 * @return array the priority list of items in array
611
	 */
612
	public function toArray()
613
	{
614
		return $this->flattenPriorities();
615
	}
616 70
617
	/**
618 70
	 * @return array the array of priorities keys with values of arrays of items.  The priorities are sorted so important priorities, lower numerics, are first.
619
	 */
620
	public function toPriorityArray()
621
	{
622
		$this->sortPriorities();
623
		return $this->_d;
624 1
	}
625
626 1
	/**
627 1
	 * Combines the map elements which have a priority below the parameter value
628
	 * @param numeric $priority the cut-off priority.  All items of priority less than this are returned.
629
	 * @param bool $inclusive whether or not the input cut-off priority is inclusive.  Default: false, not inclusive.
630
	 * @return array the array of priorities keys with values of arrays of items that are below a specified priority.
631
	 *  The priorities are sorted so important priorities, lower numerics, are first.
632
	 */
633
	public function toArrayBelowPriority($priority, $inclusive = false)
634
	{
635
		$this->sortPriorities();
636
		$items = [];
637 2
		foreach ($this->_d as $itemspriority => $itemsatpriority) {
638
			if ((!$inclusive && $itemspriority >= $priority) || $itemspriority > $priority) {
639 2
				break;
640 2
			}
641 2
			$items = array_merge($items, $itemsatpriority);
642 2
		}
643 2
		return $items;
644
	}
645 2
646
	/**
647 2
	 * Combines the map elements which have a priority above the parameter value
648
	 * @param numeric $priority the cut-off priority.  All items of priority greater than this are returned.
649
	 * @param bool $inclusive whether or not the input cut-off priority is inclusive.  Default: true, inclusive.
650
	 * @return array the array of priorities keys with values of arrays of items that are above a specified priority.
651
	 *  The priorities are sorted so important priorities, lower numerics, are first.
652
	 */
653
	public function toArrayAbovePriority($priority, $inclusive = true)
654
	{
655
		$this->sortPriorities();
656
		$items = [];
657 2
		foreach ($this->_d as $itemspriority => $itemsatpriority) {
658
			if ((!$inclusive && $itemspriority <= $priority) || $itemspriority < $priority) {
659 2
				continue;
660 2
			}
661 2
			$items = array_merge($items, $itemsatpriority);
662 2
		}
663 2
		return $items;
664
	}
665 2
666
667 2
	/**
668
	 * Copies iterable data into the priority list.
669
	 * Note, existing data in the map will be cleared first.
670
	 * @param mixed $data the data to be copied from, must be an array or object implementing Traversable
671
	 * @throws TInvalidDataTypeException If data is neither an array nor an iterator.
672
	 */
673
	public function copyFrom($data)
674
	{
675
		if ($data instanceof TPriorityList) {
676
			if ($this->getCount() > 0) {
677 27
				$this->clear();
678
			}
679 27
			foreach ($data->getPriorities() as $priority) {
680 14
				foreach ($data->itemsAtPriority($priority) as $index => $item) {
681
					$this->insertAtIndexInPriority($item, $index, $priority);
682
				}
683 14
			}
684 14
		} elseif (is_array($data) || $data instanceof \Traversable) {
685 14
			if ($this->getCount() > 0) {
686
				$this->clear();
687
			}
688 15
			foreach ($data as $key => $item) {
689 15
				$this->add($item);
690 1
			}
691
		} elseif ($data !== null) {
692 15
			throw new TInvalidDataTypeException('map_data_not_iterable');
693 15
		}
694
	}
695 1
696 1
	/**
697
	 * Merges iterable data into the priority list.
698 27
	 * New data will be appended to the end of the existing data.  If another TPriorityList is merged,
699
	 * the incoming parameter items will be appended at the priorities they are present.  These items will be added
700
	 * to the end of the existing items with equal priorities, if there are any.
701
	 * @param mixed $data the data to be merged with, must be an array or object implementing Traversable
702
	 * @throws TInvalidDataTypeException If data is neither an array nor an iterator.
703
	 */
704
	public function mergeWith($data)
705
	{
706
		if ($data instanceof TPriorityList) {
707
			foreach ($data->getPriorities() as $priority) {
708 2
				foreach ($data->itemsAtPriority($priority) as $index => $item) {
709
					$this->insertAtIndexInPriority($item, false, $priority);
710 2
				}
711 1
			}
712 1
		} elseif (is_array($data) || $data instanceof \Traversable) {
713 1
			foreach ($data as $priority => $item) {
714
				$this->add($item);
715
			}
716 1
		} elseif ($data !== null) {
717 1
			throw new TInvalidDataTypeException('map_data_not_iterable');
718 1
		}
719
	}
720 1
721 1
	/**
722
	 * Returns whether there is an element at the specified offset.
723 2
	 * This method is required by the interface \ArrayAccess.
724
	 * @param mixed $offset the offset to check on
725
	 * @return bool
726
	 */
727
	public function offsetExists($offset): bool
728
	{
729
		return ($offset >= 0 && $offset < $this->getCount());
730
	}
731 2
732
	/**
733 2
	 * Returns the element at the specified offset.
734
	 * This method is required by the interface \ArrayAccess.
735
	 * @param int $offset the offset to retrieve element.
736
	 * @return mixed the element at the offset, null if no element is found at the offset
737
	 */
738
	#[\ReturnTypeWillChange]
739
	public function offsetGet($offset)
740
	{
741
		return $this->itemAt($offset);
742 6
	}
743
744 6
	/**
745
	 * Sets the element at the specified offset. This method is required by the interface \ArrayAccess.
746
	 * Setting elements in a priority list is not straight forword when appending and setting at the
747
	 * end boundary.  When appending without an offset (a null offset), the item will be added at
748
	 * the default priority.  The item may not be the last item in the list.  When appending with an
749
	 * offset equal to the count of the list, the item will get be appended with the last items priority.
750
	 *
751
	 * All together, when setting the location of an item, the item stays in that location, but appending
752
	 * an item into a priority list doesn't mean the item is at the end of the list.
753
	 * @param int $offset the offset to set element
754
	 * @param mixed $item the element value
755
	 */
756
	public function offsetSet($offset, $item): void
757
	{
758
		if ($offset === null) {
0 ignored issues
show
introduced by
The condition $offset === null is always false.
Loading history...
759 15
			$this->add($item);
760
			return;
761 15
		}
762 12
		if ($offset === $this->getCount()) {
763
			$priority = $this->priorityAt($offset - 1, true);
764 3
			$priority[1]++;
765 1
		} else {
766 1
			$priority = $this->priorityAt($offset, true);
767
			$this->removeAtIndexInPriority($priority[1], $priority[0]);
768 2
		}
769 2
		$this->insertAtIndexInPriority($item, $priority[1], $priority[0]);
770
	}
771 3
772 3
	/**
773
	 * Unsets the element at the specified offset.
774
	 * This method is required by the interface \ArrayAccess.
775
	 * @param mixed $offset the offset to unset element
776
	 */
777
	public function offsetUnset($offset): void
778
	{
779 2
		$this->removeAt($offset);
780
	}
781
}
782