PageRenderTime 66ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/src/LinkedList.php

https://github.com/IcecaveStudios/collections
PHP | 1502 lines | 738 code | 212 blank | 552 comment | 103 complexity | 6fedc562e03bf7cd1fdf0400f735ae6b MD5 | raw file
  1. <?php
  2. namespace Icecave\Collections;
  3. use Countable;
  4. use Icecave\Collections\Iterator\Traits;
  5. use Icecave\Parity\Exception\NotComparableException;
  6. use InvalidArgumentException;
  7. use IteratorAggregate;
  8. use Serializable;
  9. use stdClass;
  10. /**
  11. * A mutable sequence with efficient addition and removal of elements.
  12. */
  13. class LinkedList implements MutableRandomAccessInterface, Countable, IteratorAggregate, Serializable
  14. {
  15. /**
  16. * @param mixed<mixed>|null $elements An iterable type containing the elements to include in this list, or null to create an empty list.
  17. */
  18. public function __construct($elements = null)
  19. {
  20. $this->clear();
  21. if (null !== $elements) {
  22. $this->insertMany(0, $elements);
  23. }
  24. }
  25. public function __clone()
  26. {
  27. $node = $this->head;
  28. $prev = null;
  29. while ($node) {
  30. // Clone the node ...
  31. $newNode = clone $node;
  32. // If there was a previous node, create the link ...
  33. if ($prev) {
  34. $prev->next = $newNode;
  35. $newNode->prev = $prev;
  36. // Otherwise this must be the head ...
  37. } else {
  38. $this->head = $newNode;
  39. }
  40. $prev = $newNode;
  41. $node = $node->next;
  42. }
  43. $this->tail = $prev;
  44. }
  45. /**
  46. * Create a LinkedList.
  47. *
  48. * @param mixed $element,... Elements to include in the collection.
  49. *
  50. * @return LinkedList
  51. */
  52. public static function create()
  53. {
  54. return new static(func_get_args());
  55. }
  56. ///////////////////////////////////////////
  57. // Implementation of CollectionInterface //
  58. ///////////////////////////////////////////
  59. /**
  60. * Fetch the number of elements in the collection.
  61. *
  62. * @see CollectionInterface::isEmpty()
  63. *
  64. * @return integer The number of elements in the collection.
  65. */
  66. public function size()
  67. {
  68. return $this->size;
  69. }
  70. /**
  71. * Check if the collection is empty.
  72. *
  73. * @return boolean True if the collection is empty; otherwise, false.
  74. */
  75. public function isEmpty()
  76. {
  77. return null === $this->head;
  78. }
  79. /**
  80. * Fetch a string representation of the collection.
  81. *
  82. * The string may not describe all elements of the collection, but should at least
  83. * provide information on the type and state of the collection.
  84. *
  85. * @return string A string representation of the collection.
  86. */
  87. public function __toString()
  88. {
  89. if ($this->isEmpty()) {
  90. return '<LinkedList 0>';
  91. }
  92. $elements = $this
  93. ->slice(0, 3)
  94. ->map('Icecave\Repr\Repr::repr');
  95. if ($this->size > 3) {
  96. $format = '<LinkedList %d [%s, ...]>';
  97. } else {
  98. $format = '<LinkedList %d [%s]>';
  99. }
  100. return sprintf(
  101. $format,
  102. $this->size,
  103. implode(', ', $elements->elements())
  104. );
  105. }
  106. //////////////////////////////////////////////////
  107. // Implementation of MutableCollectionInterface //
  108. //////////////////////////////////////////////////
  109. /**
  110. * Remove all elements from the collection.
  111. */
  112. public function clear()
  113. {
  114. $this->head = null;
  115. $this->tail = null;
  116. $this->size = 0;
  117. }
  118. //////////////////////////////////////////////
  119. // Implementation of IteratorTraitsProvider //
  120. //////////////////////////////////////////////
  121. /**
  122. * Return traits describing the collection's iteration capabilities.
  123. *
  124. * @return Traits
  125. */
  126. public function iteratorTraits()
  127. {
  128. return new Traits(true, true);
  129. }
  130. /////////////////////////////////////////
  131. // Implementation of IterableInterface //
  132. /////////////////////////////////////////
  133. /**
  134. * Fetch a native array containing the elements in the collection.
  135. *
  136. * @return array An array containing the elements in the collection.
  137. */
  138. public function elements()
  139. {
  140. $elements = array();
  141. for ($node = $this->head; null !== $node; $node = $node->next) {
  142. $elements[] = $node->element;
  143. }
  144. return $elements;
  145. }
  146. /**
  147. * Check if the collection contains an element.
  148. *
  149. * @param mixed $element The element to check.
  150. *
  151. * @return boolean True if the collection contains $element; otherwise, false.
  152. */
  153. public function contains($element)
  154. {
  155. for ($node = $this->head; null !== $node; $node = $node->next) {
  156. if ($element === $node->element) {
  157. return true;
  158. }
  159. }
  160. return false;
  161. }
  162. /**
  163. * Fetch a new collection with a subset of the elements from this collection.
  164. *
  165. * @param callable|null $predicate A predicate function used to determine which elements to include, or null to include all non-null elements.
  166. *
  167. * @return LinkedList The filtered collection.
  168. */
  169. public function filter($predicate = null)
  170. {
  171. if (null === $predicate) {
  172. $predicate = function ($element) {
  173. return null !== $element;
  174. };
  175. }
  176. $result = new static;
  177. for ($node = $this->head; null !== $node; $node = $node->next) {
  178. if (call_user_func($predicate, $node->element)) {
  179. $result->pushBack($node->element);
  180. }
  181. }
  182. return $result;
  183. }
  184. /**
  185. * Produce a new collection by applying a transformation to each element.
  186. *
  187. * The new elements produced by the transform need not be of the same type.
  188. * It is not guaranteed that the concrete type of the resulting collection will match this collection.
  189. *
  190. * @param callable $transform The transform to apply to each element.
  191. *
  192. * @return IterableInterface A new collection produced by applying $transform to each element in this collection.
  193. */
  194. public function map($transform)
  195. {
  196. $result = new static;
  197. for ($node = $this->head; null !== $node; $node = $node->next) {
  198. $result->pushBack(call_user_func($transform, $node->element));
  199. }
  200. return $result;
  201. }
  202. /**
  203. * Partitions this collection into two collections according to a predicate.
  204. *
  205. * It is not guaranteed that the concrete type of the partitioned collections will match this collection.
  206. *
  207. * @param callable $predicate A predicate function used to determine which partitioned collection to place the elements in.
  208. *
  209. * @return tuple<IterableInterface,IterableInterface> A 2-tuple containing the partitioned collections. The first collection contains the element for which the predicate returned true.
  210. */
  211. public function partition($predicate)
  212. {
  213. $left = new static;
  214. $right = new static;
  215. for ($node = $this->head; null !== $node; $node = $node->next) {
  216. if (call_user_func($predicate, $node->element)) {
  217. $left->pushBack($node->element);
  218. } else {
  219. $right->pushBack($node->element);
  220. }
  221. }
  222. return array($left, $right);
  223. }
  224. /**
  225. * Invokes the given callback on every element in the collection.
  226. *
  227. * This method behaves the same as {@see IterableInterface::map()} except that the return value of the callback is not retained.
  228. *
  229. * @param callable $callback The callback to invoke with each element.
  230. */
  231. public function each($callback)
  232. {
  233. for ($node = $this->head; null !== $node; $node = $node->next) {
  234. call_user_func($callback, $node->element);
  235. }
  236. }
  237. /**
  238. * Returns true if the given predicate returns true for all elements.
  239. *
  240. * The loop is short-circuited, exiting after the first element for which the predicate returns false.
  241. *
  242. * @param callable $predicate
  243. *
  244. * @return boolean True if $predicate($element) returns true for all elements; otherwise, false.
  245. */
  246. public function all($predicate)
  247. {
  248. for ($node = $this->head; null !== $node; $node = $node->next) {
  249. if (!call_user_func($predicate, $node->element)) {
  250. return false;
  251. }
  252. }
  253. return true;
  254. }
  255. /**
  256. * Returns true if the given predicate returns true for any element.
  257. *
  258. * The loop is short-circuited, exiting after the first element for which the predicate returns false.
  259. *
  260. * @param callable $predicate
  261. *
  262. * @return boolean True if $predicate($element) returns true for any element; otherwise, false.
  263. */
  264. public function any($predicate)
  265. {
  266. for ($node = $this->head; null !== $node; $node = $node->next) {
  267. if (call_user_func($predicate, $node->element)) {
  268. return true;
  269. }
  270. }
  271. return false;
  272. }
  273. ////////////////////////////////////////////////
  274. // Implementation of MutableIterableInterface //
  275. ////////////////////////////////////////////////
  276. /**
  277. * Filter this collection in-place.
  278. *
  279. * @param callable|null $predicate A predicate function used to determine which elements to retain, or null to retain all non-null elements.
  280. */
  281. public function filterInPlace($predicate = null)
  282. {
  283. if (null === $predicate) {
  284. $predicate = function ($element) {
  285. return null !== $element;
  286. };
  287. }
  288. $node = $this->head;
  289. $prev = null;
  290. while ($node) {
  291. // Keep the node ...
  292. if (call_user_func($predicate, $node->element)) {
  293. $prev = $node;
  294. // Don't keep the node, and it's the first one ...
  295. } elseif (null === $prev) {
  296. $this->head = $node->next;
  297. $this->head->prev = null;
  298. --$this->size;
  299. // Don't keep the node ...
  300. } else {
  301. $prev->next = $node->next;
  302. $node->next->prev = $prev;
  303. --$this->size;
  304. }
  305. $node = $node->next;
  306. }
  307. $this->tail = $prev;
  308. }
  309. /**
  310. * Replace each element in the collection with the result of a transformation on that element.
  311. *
  312. * The new elements produced by the transform must be the same type.
  313. *
  314. * @param callable $transform The transform to apply to each element.
  315. */
  316. public function mapInPlace($transform)
  317. {
  318. for ($node = $this->head; null !== $node; $node = $node->next) {
  319. $node->element = call_user_func($transform, $node->element);
  320. }
  321. }
  322. /////////////////////////////////////////
  323. // Implementation of SequenceInterface //
  324. /////////////////////////////////////////
  325. /**
  326. * Fetch the first element in the sequence.
  327. *
  328. * @return mixed The first element in the sequence.
  329. * @throws Exception\EmptyCollectionException if the collection is empty.
  330. */
  331. public function front()
  332. {
  333. if ($this->isEmpty()) {
  334. throw new Exception\EmptyCollectionException;
  335. }
  336. return $this->head->element;
  337. }
  338. /**
  339. * Fetch the first element in the sequence.
  340. *
  341. * @param mixed &$element Assigned the element at the front of collection.
  342. *
  343. * @return boolean True is the element exists and was assigned to $element; otherwise, false.
  344. */
  345. public function tryFront(&$element)
  346. {
  347. if ($this->isEmpty()) {
  348. return false;
  349. }
  350. $element = $this->head->element;
  351. return true;
  352. }
  353. /**
  354. * Fetch the last element in the sequence.
  355. *
  356. * @return mixed The first element in the sequence.
  357. * @throws Exception\EmptyCollectionException if the collection is empty.
  358. */
  359. public function back()
  360. {
  361. if ($this->isEmpty()) {
  362. throw new Exception\EmptyCollectionException;
  363. }
  364. return $this->tail->element;
  365. }
  366. /**
  367. * Fetch the last element in the sequence.
  368. *
  369. * @param mixed &$element Assigned the element at the front of collection.
  370. *
  371. * @return boolean True is the element exists and was assigned to $element; otherwise, false.
  372. */
  373. public function tryBack(&$element)
  374. {
  375. if ($this->isEmpty()) {
  376. return false;
  377. }
  378. $element = $this->tail->element;
  379. return true;
  380. }
  381. /**
  382. * Create a new sequence with the elements from this sequence in sorted order.
  383. *
  384. * @param callable|null $comparator A strcmp style comparator function.
  385. *
  386. * @return LinkedList
  387. */
  388. public function sort($comparator = null)
  389. {
  390. $result = clone $this;
  391. $result->sortInPlace($comparator);
  392. return $result;
  393. }
  394. /**
  395. * Create a new sequence with the elements from this sequence in reverse order.
  396. *
  397. * It is not guaranteed that the concrete type of the reversed collection will match this collection.
  398. *
  399. * @return LinkedList The reversed sequence.
  400. */
  401. public function reverse()
  402. {
  403. $result = new static;
  404. for ($node = $this->head; null !== $node; $node = $node->next) {
  405. $result->pushFront($node->element);
  406. }
  407. return $result;
  408. }
  409. /**
  410. * Create a new sequence by appending the elements in the given sequence to this sequence.
  411. *
  412. * @param mixed<mixed> $sequence The sequence to append.
  413. * @param mixed<mixed> $additional,... Additional sequences to append.
  414. *
  415. * @return SequenceInterface A new sequence containing all elements from this sequence and $sequence.
  416. */
  417. public function join($sequence)
  418. {
  419. $result = new static;
  420. list($result->head, $result->tail, $result->size) = $this->cloneNodes($this->head);
  421. foreach (func_get_args() as $sequence) {
  422. $result->insertMany($result->size, $sequence);
  423. }
  424. return $result;
  425. }
  426. ////////////////////////////////////////////////
  427. // Implementation of MutableSequenceInterface //
  428. ////////////////////////////////////////////////
  429. /**
  430. * Sort this sequence in-place.
  431. *
  432. * @link http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
  433. *
  434. * @param callable|null $comparator A strcmp style comparator function.
  435. */
  436. public function sortInPlace($comparator = null)
  437. {
  438. if ($this->size <= 1) {
  439. return;
  440. }
  441. if (null === $comparator) {
  442. $comparator = function ($a, $b) {
  443. if ($a < $b) {
  444. return -1;
  445. } elseif ($a > $b) {
  446. return 1;
  447. } else {
  448. return 0;
  449. }
  450. };
  451. }
  452. $chunkSize = 1;
  453. $left = null;
  454. $head = $this->head;
  455. $tail = null;
  456. do {
  457. $left = $head;
  458. $head = null;
  459. $tail = null;
  460. $mergeCount = 0;
  461. while ($left) {
  462. ++$mergeCount;
  463. $right = $left;
  464. for ($leftSize = 0; $right && $leftSize < $chunkSize; ++$leftSize) {
  465. $right = $right->next;
  466. }
  467. $rightSize = $chunkSize;
  468. while ($leftSize || ($right && $rightSize)) {
  469. if (0 === $leftSize) {
  470. $node = $right;
  471. $right = $right->next;
  472. --$rightSize;
  473. } elseif (!$right || 0 === $rightSize) {
  474. $node = $left;
  475. $left = $left->next;
  476. --$leftSize;
  477. } elseif (call_user_func($comparator, $left->element, $right->element) <= 0) {
  478. $node = $left;
  479. $left = $left->next;
  480. --$leftSize;
  481. } else {
  482. $node = $right;
  483. $right = $right->next;
  484. --$rightSize;
  485. }
  486. if ($tail) {
  487. $tail->next = $node;
  488. $node->prev = $tail;
  489. } else {
  490. $head = $node;
  491. }
  492. $tail = $node;
  493. }
  494. $left = $right;
  495. }
  496. $tail->next = null;
  497. $chunkSize *= 2;
  498. } while ($mergeCount > 1);
  499. $this->head = $head;
  500. $this->tail = $tail;
  501. }
  502. /**
  503. * Reverse this sequence in-place.
  504. */
  505. public function reverseInPlace()
  506. {
  507. $prev = null;
  508. $node = $this->head;
  509. while ($node) {
  510. $next = $node->next;
  511. $node->next = $prev;
  512. $node->prev = $next;
  513. $prev = $node;
  514. $node = $next;
  515. }
  516. $head = $this->head;
  517. $this->head = $this->tail;
  518. $this->tail = $head;
  519. }
  520. /**
  521. * Appending elements in the given sequence to this sequence.
  522. *
  523. * @param mixed<mixed> $sequence The sequence to append.
  524. * @param mixed<mixed> $additional,... Additional sequences to append.
  525. */
  526. public function append($sequence)
  527. {
  528. foreach (func_get_args() as $sequence) {
  529. $this->insertMany($this->size, $sequence);
  530. }
  531. }
  532. /**
  533. * Add a new element to the front of the sequence.
  534. *
  535. * @param mixed $element The element to prepend.
  536. */
  537. public function pushFront($element)
  538. {
  539. $this->head = $this->createNode($element, $this->head);
  540. if (0 === $this->size++) {
  541. $this->tail = $this->head;
  542. } else {
  543. $this->head->next->prev = $this->head;
  544. }
  545. }
  546. /**
  547. * Remove and return the element at the front of the sequence.
  548. *
  549. * @return mixed The element at the front of the sequence.
  550. * @throws Exception\EmptyCollectionException if the collection is empty.
  551. */
  552. public function popFront()
  553. {
  554. if ($this->isEmpty()) {
  555. throw new Exception\EmptyCollectionException;
  556. }
  557. $element = $this->head->element;
  558. $this->head = $this->head->next;
  559. if (0 === --$this->size) {
  560. $this->tail = null;
  561. } else {
  562. $this->head->prev = null;
  563. }
  564. return $element;
  565. }
  566. /**
  567. * Remove the element at the front of the sequence.
  568. *
  569. * @param mixed &$element Assigned the removed element.
  570. *
  571. * @return boolean True if the front element is removed and assigned to $element; otherwise, false.
  572. */
  573. public function tryPopFront(&$element = null)
  574. {
  575. if ($this->isEmpty()) {
  576. return false;
  577. }
  578. $element = $this->popFront();
  579. return true;
  580. }
  581. /**
  582. * Add a new element to the back of the sequence.
  583. *
  584. * @param mixed $element The element to append.
  585. */
  586. public function pushBack($element)
  587. {
  588. $node = $this->createNode($element, null, $this->tail);
  589. if (0 === $this->size++) {
  590. $this->head = $node;
  591. } else {
  592. $this->tail->next = $node;
  593. }
  594. $this->tail = $node;
  595. }
  596. /**
  597. * Remove and return the element at the back of the sequence.
  598. *
  599. * @return mixed The element at the back of the sequence.
  600. * @throws Exception\EmptyCollectionException if the collection is empty.
  601. */
  602. public function popBack()
  603. {
  604. if ($this->isEmpty()) {
  605. throw new Exception\EmptyCollectionException;
  606. }
  607. $element = $this->tail->element;
  608. if (0 === --$this->size) {
  609. $this->head = null;
  610. $this->tail = null;
  611. } else {
  612. $this->tail = $this->nodeAt($this->size - 1);
  613. $this->tail->next->prev = null;
  614. $this->tail->next = null;
  615. }
  616. return $element;
  617. }
  618. /**
  619. * Remove the element at the back of the sequence.
  620. *
  621. * @param mixed &$element Assigned the removed element.
  622. *
  623. * @return boolean True if the back element is removed and assigned to $element; otherwise, false.
  624. */
  625. public function tryPopBack(&$element = null)
  626. {
  627. if ($this->isEmpty()) {
  628. return false;
  629. }
  630. $element = $this->popBack();
  631. return true;
  632. }
  633. /**
  634. * Resize the sequence.
  635. *
  636. * @param integer $size The new size of the collection.
  637. * @param mixed $element The value to use for populating new elements when $size > $this->size().
  638. */
  639. public function resize($size, $element = null)
  640. {
  641. if ($this->size > $size) {
  642. $this->removeMany($size);
  643. } else {
  644. while ($size--) {
  645. $this->pushBack($element);
  646. }
  647. }
  648. }
  649. /////////////////////////////////////////////
  650. // Implementation of RandomAccessInterface //
  651. /////////////////////////////////////////////
  652. /**
  653. * Fetch the element at the given index.
  654. *
  655. * @param mixed $index The index of the element to fetch, if index is a negative number the element that far from the end of the sequence is returned.
  656. *
  657. * @return mixed The element at $index.
  658. * @throws Exception\IndexException if $index is out of range.
  659. */
  660. public function get($index)
  661. {
  662. $this->validateIndex($index);
  663. return $this->nodeAt($index)->element;
  664. }
  665. /**
  666. * Extract a range of elements.
  667. *
  668. * It is not guaranteed that the concrete type of the slice collection will match this collection.
  669. *
  670. * @param integer $index The index from which the slice will start. If index is a negative number the slice will begin that far from the end of the sequence.
  671. * @param integer|null $count The maximum number of elements to include in the slice, or null to include all elements from $index to the end of the sequence.
  672. *
  673. * @return SequenceInterface The sliced sequence.
  674. * @throws Exception\IndexException if $index is out of range.
  675. */
  676. public function slice($index, $count = null)
  677. {
  678. $this->validateIndex($index);
  679. $start = $this->nodeAt($index);
  680. if (null === $count) {
  681. $stop = null;
  682. } else {
  683. $count = max(0, min($this->size - $index, $count));
  684. $stop = $this->nodeFrom($start, $count);
  685. }
  686. $result = new static;
  687. list($result->head, $result->tail, $result->size) = $this->cloneNodes($start, $stop);
  688. return $result;
  689. }
  690. /**
  691. * Extract a range of elements.
  692. *
  693. * It is not guaranteed that the concrete type of the slice collection will match this collection.
  694. *
  695. * Extracts all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  696. *
  697. * @param integer $begin The index from which the slice will start. If begin is a negative number the slice will begin that far from the end of the sequence.
  698. * @param integer $end The index at which the slice will end. If end is a negative number the slice will end that far from the end of the sequence.
  699. *
  700. * @return SequenceInterface The sliced sequence.
  701. * @throws Exception\IndexException if $begin or $end is out of range.
  702. */
  703. public function range($begin, $end)
  704. {
  705. $this->validateIndex($begin);
  706. $this->validateIndex($end, $this->size);
  707. return $this->slice($begin, $end - $begin);
  708. }
  709. /**
  710. * Find the index of the first instance of a particular element in the sequence.
  711. *
  712. * Searches all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  713. *
  714. * @param mixed $element The element to search for.
  715. * @param integer $begin The index to start searching from.
  716. * @param integer|null $end The index to to stop searching at, or null to search to the end of the sequence.
  717. *
  718. * @return integer|null The index of the element, or null if is not present in the sequence.
  719. * @throws Exception\IndexException if $begin or $end is out of range.
  720. */
  721. public function indexOf($element, $begin = 0, $end = null)
  722. {
  723. $predicate = function ($e) use ($element) {
  724. return $element === $e;
  725. };
  726. return $this->find($predicate, $begin, $end);
  727. }
  728. /**
  729. * Find the index of the last instance of a particular element in the sequence.
  730. *
  731. * Searches all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  732. *
  733. * @param mixed $element The element to search for.
  734. * @param integer $begin The index to start searching from.
  735. * @param integer|null $end The index to to stop searching at, or null to search to the end of the sequence.
  736. *
  737. * @return integer|null The index of the element, or null if is not present in the sequence.
  738. * @throws Exception\IndexException if $begin is out of range.
  739. */
  740. public function indexOfLast($element, $begin = 0, $end = null)
  741. {
  742. $predicate = function ($e) use ($element) {
  743. return $element === $e;
  744. };
  745. return $this->findLast($predicate, $begin, $end);
  746. }
  747. /**
  748. * Find the index of the first instance of an element matching given criteria.
  749. *
  750. * Searches all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  751. *
  752. * @param callable $predicate A predicate function used to determine which element constitutes a match.
  753. * @param integer $begin The index to start searching from.
  754. * @param integer|null $end The index to to stop searching at, or null to search to the end of the sequence.
  755. *
  756. * @return integer|null The index of the element, or null if is not present in the sequence.
  757. * @throws Exception\IndexException if $begin is out of range.
  758. */
  759. public function find($predicate, $begin = 0, $end = null)
  760. {
  761. if ($this->isEmpty()) {
  762. return null;
  763. }
  764. $this->validateIndex($begin);
  765. $this->validateIndex($end, $this->size);
  766. $node = $this->nodeAt($begin);
  767. while (null !== $node && $begin !== $end) {
  768. if (call_user_func($predicate, $node->element)) {
  769. return $begin;
  770. }
  771. ++$begin;
  772. $node = $node->next;
  773. }
  774. return null;
  775. }
  776. /**
  777. * Find the index of the last instance of an element matching given criteria.
  778. *
  779. * Searches all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  780. *
  781. * @param callable $predicate A predicate function used to determine which element constitutes a match.
  782. * @param integer $begin The index to start searching from.
  783. * @param integer|null $end The index to to stop searching at, or null to search to the end of the sequence.
  784. *
  785. * @return integer|null The index of the element, or null if is not present in the sequence.
  786. * @throws Exception\IndexException if $begin is out of range.
  787. */
  788. public function findLast($predicate, $begin = 0, $end = null)
  789. {
  790. if ($this->isEmpty()) {
  791. return null;
  792. }
  793. $this->validateIndex($begin);
  794. $this->validateIndex($end, $this->size);
  795. $node = $this->nodeAt(--$end);
  796. while (null !== $node && $end >= $begin) {
  797. if (call_user_func($predicate, $node->element)) {
  798. return $end;
  799. }
  800. --$end;
  801. $node = $node->prev;
  802. }
  803. return null;
  804. }
  805. ////////////////////////////////////////////////////
  806. // Implementation of MutableRandomAccessInterface //
  807. ////////////////////////////////////////////////////
  808. /**
  809. * Replace the element at a particular position in the sequence.
  810. *
  811. * @param integer $index The index of the element to set, if index is a negative number the element that far from the end of the sequence is set.
  812. * @param mixed $element The element to set.
  813. *
  814. * @throws Exception\IndexException if $index is out of range.
  815. */
  816. public function set($index, $element)
  817. {
  818. $this->validateIndex($index);
  819. $this->nodeAt($index)->element = $element;
  820. }
  821. /**
  822. * Insert an element at a particular index.
  823. *
  824. * @param integer $index The index at which the element is inserted, if index is a negative number the element is inserted that far from the end of the sequence.
  825. * @param mixed $element The element to insert.
  826. *
  827. * @throws Exception\IndexException if $index is out of range.
  828. */
  829. public function insert($index, $element)
  830. {
  831. $this->insertMany($index, array($element));
  832. }
  833. /**
  834. * Insert all elements from another collection at a particular index.
  835. *
  836. * @param integer $index The index at which the elements are inserted, if index is a negative number the elements are inserted that far from the end of the sequence.
  837. * @param mixed<mixed> $elements The elements to insert.
  838. */
  839. public function insertMany($index, $elements)
  840. {
  841. $this->validateIndex($index, $this->size);
  842. list($head, $tail, $size) = $this->createNodes($elements);
  843. $this->insertNodes($index, $head, $tail, $size);
  844. }
  845. /**
  846. * Insert a sub-range of another collection at a particular index.
  847. *
  848. * Inserts all elements from the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  849. *
  850. * @param integer $index The index at which the elements are inserted, if index is a negative number the elements are inserted that far from the end of the sequence.
  851. * @param RandomAccessInterface+LinkedList $elements The elements to insert.
  852. * @param integer $begin The index of the first element from $elements to insert, if begin is a negative number the removal begins that far from the end of the sequence.
  853. * @param integer $end|null The index of the last element to $elements to insert, if end is a negative number the removal ends that far from the end of the sequence.
  854. *
  855. * @throws Exception\IndexException if $index, $begin or $end is out of range.
  856. */
  857. public function insertRange($index, RandomAccessInterface $elements, $begin, $end = null)
  858. {
  859. if (!$elements instanceof self) {
  860. throw new InvalidArgumentException('The given collection is not an instance of ' . __CLASS__ . '.');
  861. }
  862. $this->validateIndex($index);
  863. $elements->validateIndex($begin);
  864. $elements->validateIndex($end, $elements->size);
  865. list($head, $tail, $size) = $elements->cloneNodes(
  866. $elements->nodeAt($begin),
  867. null,
  868. $end - $begin
  869. );
  870. $this->insertNodes($index, $head, $tail, $size);
  871. }
  872. /**
  873. * Remove the element at a given index.
  874. *
  875. * Elements after the given endex are moved forward by one.
  876. *
  877. * @param integer $index The index of the element to remove, if index is a negative number the element that far from the end of the sequence is removed.
  878. *
  879. * @throws Exception\IndexException if $index is out of range.
  880. */
  881. public function remove($index)
  882. {
  883. $this->removeRange($index, $index + 1);
  884. }
  885. /**
  886. * Remove a range of elements at a given index.
  887. *
  888. * @param integer $index The index of the first element to remove, if index is a negative number the removal begins that far from the end of the sequence.
  889. * @param integer|null $count The number of elements to remove, or null to remove all elements up to the end of the sequence.
  890. *
  891. * @throws Exception\IndexException if $index is out of range.
  892. */
  893. public function removeMany($index, $count = null)
  894. {
  895. $this->validateIndex($index);
  896. // Remove, but not all the way to the end ...
  897. if (null !== $count && $count < $this->size - $index) {
  898. $count = max(0, $count);
  899. $node = $this->nodeAt($index - 1);
  900. $node->next->prev = null;
  901. $node->next = $this->nodeFrom($node, $count + 1);
  902. $node->next->prev = $node;
  903. $this->size -= $count;
  904. // Remove everything ...
  905. } elseif (0 === $index) {
  906. $this->clear();
  907. // Remove everything to the end ...
  908. } else {
  909. $node = $this->nodeAt($index - 1);
  910. $node->next->prev = null;
  911. $node->next = null;
  912. $this->tail = $node;
  913. $this->size = $index;
  914. }
  915. }
  916. /**
  917. * Remove a range of elements at a given index.
  918. *
  919. * Removes all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  920. *
  921. * @param integer $begin The index of the first element to remove, if $begin is a negative number the removal begins that far from the end of the sequence.
  922. * @param integer $end The index of the last element to remove, if $end is a negative number the removal ends that far from the end of the sequence.
  923. *
  924. * @throws Exception\IndexException if $begin or $end is out of range.
  925. */
  926. public function removeRange($begin, $end)
  927. {
  928. $this->validateIndex($begin);
  929. $this->validateIndex($end, $this->size);
  930. $this->removeMany($begin, $end - $begin);
  931. }
  932. /**
  933. * Replace a range of elements with a second set of elements.
  934. *
  935. * @param integer $index The index of the first element to replace, if index is a negative number the replace begins that far from the end of the sequence.
  936. * @param mixed<mixed> $elements The elements to insert.
  937. * @param integer|null $count The number of elements to replace, or null to replace all elements up to the end of the sequence.
  938. */
  939. public function replace($index, $elements, $count = null)
  940. {
  941. $this->validateIndex($index);
  942. $this->removeMany($index, $count);
  943. $this->insertMany($index, $elements);
  944. }
  945. /**
  946. * Replace a range of elements with a second set of elements.
  947. *
  948. * Replaces all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  949. *
  950. * @param integer $begin The index of the first element to replace, if begin is a negative number the replace begins that far from the end of the sequence.
  951. * @param integer $end The index of the last element to replace, if end is a negativ enumber the replace ends that far from the end of the sequence.
  952. * @param mixed<mixed> $elements The elements to insert.
  953. */
  954. public function replaceRange($begin, $end, $elements)
  955. {
  956. $this->validateIndex($begin);
  957. $this->removeRange($begin, $end);
  958. $this->insertMany($begin, $elements);
  959. }
  960. /**
  961. * Swap the elements at two index positions.
  962. *
  963. * @param integer $index1 The index of the first element.
  964. * @param integer $index2 The index of the second element.
  965. *
  966. * @throws Exception\IndexException if $index1 or $index2 is out of range.
  967. */
  968. public function swap($index1, $index2)
  969. {
  970. $this->validateIndex($index1);
  971. $this->validateIndex($index2);
  972. $this->doSwap($index1, $index2);
  973. }
  974. /**
  975. * Swap the elements at two index positions.
  976. *
  977. * @param integer $index1 The index of the first element.
  978. * @param integer $index2 The index of the second element.
  979. *
  980. * @return boolean True if $index1 and $index2 are in range and the swap is successful.
  981. */
  982. public function trySwap($index1, $index2)
  983. {
  984. if ($index1 < 0) {
  985. $index1 += $this->size();
  986. }
  987. if ($index2 < 0) {
  988. $index2 += $this->size();
  989. }
  990. if ($index1 < 0 || $index1 >= $this->size()) {
  991. return false;
  992. }
  993. if ($index2 < 0 || $index2 >= $this->size()) {
  994. return false;
  995. }
  996. $this->doSwap($index1, $index2);
  997. return true;
  998. }
  999. /////////////////////////////////
  1000. // Implementation of Countable //
  1001. /////////////////////////////////
  1002. public function count()
  1003. {
  1004. return $this->size();
  1005. }
  1006. /////////////////////////////////////////
  1007. // Implementation of IteratorAggregate //
  1008. /////////////////////////////////////////
  1009. public function getIterator()
  1010. {
  1011. return new Detail\LinkedListIterator($this->head, $this->tail);
  1012. }
  1013. ////////////////////////////////////
  1014. // Implementation of Serializable //
  1015. ////////////////////////////////////
  1016. /**
  1017. * Serialize the collection.
  1018. *
  1019. * @return string The serialized data.
  1020. */
  1021. public function serialize()
  1022. {
  1023. return serialize($this->elements());
  1024. }
  1025. /**
  1026. * Unserialize collection data.
  1027. *
  1028. * @param string $packet The serialized data.
  1029. */
  1030. public function unserialize($packet)
  1031. {
  1032. $elements = unserialize($packet);
  1033. $this->__construct($elements);
  1034. }
  1035. ///////////////////////////////////////////
  1036. // Implementation of ComparableInterface //
  1037. ///////////////////////////////////////////
  1038. /**
  1039. * Compare this object with another value, yielding a result according to the following table:
  1040. *
  1041. * +--------------------+---------------+
  1042. * | Condition | Result |
  1043. * +--------------------+---------------+
  1044. * | $this == $value | $result === 0 |
  1045. * | $this < $value | $result < 0 |
  1046. * | $this > $value | $result > 0 |
  1047. * +--------------------+---------------+
  1048. *
  1049. * @param mixed $value The value to compare.
  1050. *
  1051. * @return integer The result of the comparison.
  1052. * @throws Icecave\Parity\Exception\NotComparableException Indicates that the implementation does not know how to compare $this to $value.
  1053. */
  1054. public function compare($value)
  1055. {
  1056. if (!$this->canCompare($value)) {
  1057. throw new NotComparableException($this, $value);
  1058. }
  1059. return Collection::compare($this, $value);
  1060. }
  1061. /////////////////////////////////////////////////////
  1062. // Implementation of RestrictedComparableInterface //
  1063. /////////////////////////////////////////////////////
  1064. /**
  1065. * Check if $this is able to be compared to another value.
  1066. *
  1067. * A return value of false indicates that calling $this->compare($value)
  1068. * will throw an exception.
  1069. *
  1070. * @param mixed $value The value to compare.
  1071. *
  1072. * @return boolean True if $this can be compared to $value.
  1073. */
  1074. public function canCompare($value)
  1075. {
  1076. return is_object($value)
  1077. && __CLASS__ === get_class($value);
  1078. }
  1079. ///////////////////////////////////////////////////
  1080. // Implementation of ExtendedComparableInterface //
  1081. ///////////////////////////////////////////////////
  1082. /**
  1083. * @param mixed $value The value to compare.
  1084. *
  1085. * @return boolean True if $this == $value.
  1086. */
  1087. public function isEqualTo($value)
  1088. {
  1089. return $this->compare($value) === 0;
  1090. }
  1091. /**
  1092. * @param mixed $value The value to compare.
  1093. *
  1094. * @return boolean True if $this != $value.
  1095. */
  1096. public function isNotEqualTo($value)
  1097. {
  1098. return $this->compare($value) !== 0;
  1099. }
  1100. /**
  1101. * @param mixed $value The value to compare.
  1102. *
  1103. * @return boolean True if $this < $value.
  1104. */
  1105. public function isLessThan($value)
  1106. {
  1107. return $this->compare($value) < 0;
  1108. }
  1109. /**
  1110. * @param mixed $value The value to compare.
  1111. *
  1112. * @return boolean True if $this > $value.
  1113. */
  1114. public function isGreaterThan($value)
  1115. {
  1116. return $this->compare($value) > 0;
  1117. }
  1118. /**
  1119. * @param mixed $value The value to compare.
  1120. *
  1121. * @return boolean True if $this <= $value.
  1122. */
  1123. public function isLessThanOrEqualTo($value)
  1124. {
  1125. return $this->compare($value) <= 0;
  1126. }
  1127. /**
  1128. * @param mixed $value The value to compare.
  1129. *
  1130. * @return boolean True if $this >= $value.
  1131. */
  1132. public function isGreaterThanOrEqualTo($value)
  1133. {
  1134. return $this->compare($value) >= 0;
  1135. }
  1136. ////////////////////////////
  1137. // Model specific methods //
  1138. ////////////////////////////
  1139. /**
  1140. * @param integer $index1
  1141. * @param integer $index2
  1142. */
  1143. private function doSwap($index1, $index2)
  1144. {
  1145. $a = min($index1, $index2);
  1146. $b = max($index1, $index2);
  1147. $node1 = $this->nodeAt($a);
  1148. $node2 = $this->nodeFrom($node1, $b - $a);
  1149. $element = $node1->element;
  1150. $node1->element = $node2->element;
  1151. $node2->element = $element;
  1152. }
  1153. /**
  1154. * @param integer &$index
  1155. * @param integer|null $max
  1156. */
  1157. private function validateIndex(&$index, $max = null)
  1158. {
  1159. if (null === $max) {
  1160. $max = $this->size - 1;
  1161. }
  1162. if (null === $index) {
  1163. $index = $max;
  1164. } elseif ($index < 0) {
  1165. $index += $this->size;
  1166. }
  1167. if ($index < 0 || $index > $max) {
  1168. throw new Exception\IndexException($index);
  1169. }
  1170. }
  1171. /**
  1172. * @param mixed $element
  1173. * @param stdClass|null $next
  1174. * @param stdClass|null $prev
  1175. */
  1176. private function createNode($element = null, stdClass $next = null, stdClass $prev = null)
  1177. {
  1178. $node = new stdClass;
  1179. $node->next = $next;
  1180. $node->prev = $prev;
  1181. $node->element = $element;
  1182. return $node;
  1183. }
  1184. /**
  1185. * @param integer $index
  1186. */
  1187. private function nodeAt($index)
  1188. {
  1189. $half = $this->size / 2;
  1190. if ($index <= $half) {
  1191. return $this->nodeFrom($this->head, $index);
  1192. }
  1193. return $this->nodeFromReverse($this->tail, $this->size - $index - 1);
  1194. }
  1195. /**
  1196. * @param stdClass $node
  1197. * @param integer $count
  1198. */
  1199. private function nodeFrom(stdClass $node, $count)
  1200. {
  1201. while ($node && $count--) {
  1202. $node = $node->next;
  1203. }
  1204. return $node;
  1205. }
  1206. private function nodeFromReverse(stdClass $node, $count)
  1207. {
  1208. while ($node && $count--) {
  1209. $node = $node->prev;
  1210. }
  1211. return $node;
  1212. }
  1213. /**
  1214. * @param stdClass $start
  1215. * @param stdClass|null $stop
  1216. * @param integer|null $limit
  1217. */
  1218. private function cloneNodes(stdClass $start, stdClass $stop = null, $limit = null)
  1219. {
  1220. $head = null;
  1221. $tail = null;
  1222. $size = 0;
  1223. for ($node = $start; $stop !== $node && $size !== $limit; $node = $node->next) {
  1224. $n = $this->createNode($node->element);
  1225. if (null === $head) {
  1226. $head = $n;
  1227. } else {
  1228. $tail->next = $n;
  1229. $n->prev = $tail;
  1230. }
  1231. $tail = $n;
  1232. ++$size;
  1233. }
  1234. return array($head, $tail, $size);
  1235. }
  1236. /**
  1237. * @param mixed<mixed> $elements
  1238. */
  1239. private function createNodes($elements)
  1240. {
  1241. $head = null;
  1242. $tail = null;
  1243. $size = 0;
  1244. foreach ($elements as $element) {
  1245. $node = $this->createNode($element);
  1246. if (null === $head) {
  1247. $head = $node;
  1248. } else {
  1249. $tail->next = $node;
  1250. $node->prev = $tail;
  1251. }
  1252. $tail = $node;
  1253. ++$size;
  1254. }
  1255. return array($head, $tail, $size);
  1256. }
  1257. /**
  1258. * @param integer $index
  1259. * @param stdClass|null $head
  1260. * @param stdClass|null $tail
  1261. * @param integer $size
  1262. */
  1263. private function insertNodes($index, stdClass $head = null, stdClass $tail = null, $size)
  1264. {
  1265. if (null === $head) {
  1266. return;
  1267. } elseif (0 === $this->size) {
  1268. $this->head = $head;
  1269. $this->tail = $tail;
  1270. } elseif (0 === $index) {
  1271. $tail->next = $this->head;
  1272. $this->head->prev = $tail;
  1273. $this->head = $head;
  1274. } elseif ($this->size === $index) {
  1275. $this->tail->next = $head;
  1276. $head->prev = $this->tail;
  1277. $this->tail = $tail;
  1278. } else {
  1279. $node = $this->nodeAt($index - 1);
  1280. $tail->next = $node->next;
  1281. $node->next->prev = $tail;
  1282. $node->next = $head;
  1283. $head->prev = $node;
  1284. }
  1285. $this->size += $size;
  1286. }
  1287. private $head;
  1288. private $tail;
  1289. private $size;
  1290. }