PageRenderTime 60ms CodeModel.GetById 28ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Vector.php

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