PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Collection.php

https://github.com/IcecaveStudios/collections
PHP | 774 lines | 438 code | 89 blank | 247 comment | 56 complexity | dcb1a8ea2b13d0edc4bb88b839da7d81 MD5 | raw file
  1. <?php
  2. namespace Icecave\Collections;
  3. use ArrayAccess;
  4. use ArrayIterator;
  5. use Countable;
  6. use Icecave\Collections\Iterator\Traits;
  7. use Icecave\Collections\Iterator\TraitsProviderInterface;
  8. use Icecave\Repr\Repr;
  9. use InvalidArgumentException;
  10. use Iterator;
  11. use IteratorAggregate;
  12. use SplDoublyLinkedList;
  13. use SplFixedArray;
  14. use SplHeap;
  15. use SplObjectStorage;
  16. use SplPriorityQueue;
  17. use Traversable;
  18. /**
  19. * Utility functions for working with arbitrary collection types.
  20. */
  21. abstract class Collection
  22. {
  23. /**
  24. * Check if a collection is empty.
  25. *
  26. * @param array|Traversable|CollectionInterface $collection
  27. *
  28. * @return boolean True if $collection is contains zero elements; otherwise false.
  29. */
  30. public static function isEmpty($collection)
  31. {
  32. if ($collection instanceof CollectionInterface) {
  33. return $collection->isEmpty();
  34. }
  35. return 0 === static::size($collection);
  36. }
  37. /**
  38. * Get the number of elements in a collection.
  39. *
  40. * @param array|Traversable|Countable|CollectionInterface $collection
  41. *
  42. * @return integer The number of elements in $collection
  43. */
  44. public static function size($collection)
  45. {
  46. $size = static::trySize($collection);
  47. if (null !== $size) {
  48. return $size;
  49. }
  50. $count = 0;
  51. foreach ($collection as $value) {
  52. ++$count;
  53. }
  54. return $count;
  55. }
  56. /**
  57. * Attempt to get the number of elements in a collection.
  58. *
  59. * @param mixed $collection
  60. *
  61. * @return integer|null The number of elements in $collection, or null if the size can not be determined without iterating the entire collection.
  62. */
  63. public static function trySize($collection)
  64. {
  65. if ($collection instanceof CollectionInterface) {
  66. return $collection->size();
  67. } elseif ($collection instanceof Countable) {
  68. return count($collection);
  69. } elseif (is_array($collection)) {
  70. return count($collection);
  71. }
  72. return null;
  73. }
  74. /**
  75. * Fetch the value associated with the given key.
  76. *
  77. * @param array|Traversable|AssociativeInterface $collection
  78. * @param mixed $key The key to fetch.
  79. *
  80. * @return mixed The associated value.
  81. * @throws Exception\UnknownKeyException if no such key exists.
  82. */
  83. public static function get($collection, $key)
  84. {
  85. $value = null;
  86. if (static::tryGet($collection, $key, $value)) {
  87. return $value;
  88. }
  89. throw new Exception\UnknownKeyException($key);
  90. }
  91. /**
  92. * Fetch the value associated with the given key if it exists.
  93. *
  94. * @param array|Traversable|AssociativeInterface $collection
  95. * @param mixed $key The key to fetch.
  96. * @param mixed &$value Assigned the value associated with $key if it exists.
  97. *
  98. * @return boolean True if $key exists and $value was populated; otherwise, false.
  99. */
  100. public static function tryGet($collection, $key, &$value)
  101. {
  102. if ($collection instanceof AssociativeInterface) {
  103. return $collection->tryGet($key, $value);
  104. } elseif (is_array($collection)) {
  105. if (array_key_exists($key, $collection)) {
  106. $value = $collection[$key];
  107. return true;
  108. }
  109. return false;
  110. }
  111. return static::any(
  112. $collection,
  113. function ($k, $v) use ($key, &$value) {
  114. if ($k === $key) {
  115. $value = $v;
  116. return true;
  117. }
  118. return false;
  119. }
  120. );
  121. }
  122. /**
  123. * Fetch the value associated with the given key, or a default value if it does not exist.
  124. *
  125. * @param array|Traversable|IterableInterface $collection
  126. * @param mixed $key The key to fetch.
  127. * @param mixed $default The default value to return if $key does not exist.
  128. *
  129. * @return mixed The value associated with $key, or the $default if nos such key exists.
  130. */
  131. public static function getWithDefault($collection, $key, $default = null)
  132. {
  133. $value = null;
  134. if (static::tryGet($collection, $key, $value)) {
  135. return $value;
  136. }
  137. return $default;
  138. }
  139. /**
  140. * Check if the collection contains an element with the given key.
  141. *
  142. * @param array|Traversable|IterableInterface $collection
  143. * @param mixed $key The key to check.
  144. *
  145. * @return boolean True if the collection contains the given key; otherwise, false.
  146. */
  147. public static function hasKey($collection, $key)
  148. {
  149. if ($collection instanceof AssociativeInterface) {
  150. return $collection->hasKey($key);
  151. } elseif ($collection instanceof SequenceInterface) {
  152. return is_int($key) && $key >= 0 && $key < $collection->size();
  153. } elseif (is_array($collection)) {
  154. return array_key_exists($key, $collection);
  155. }
  156. return static::any(
  157. $collection,
  158. function ($k, $v) use ($key) {
  159. return $k === $key;
  160. }
  161. );
  162. }
  163. /**
  164. * Check if the collection contains an element with the given value.
  165. *
  166. * @param array|Traversable|IterableInterface $collection
  167. * @param mixed $value The value to check.
  168. *
  169. * @return boolean True if the collection contains $value; otherwise, false.
  170. */
  171. public static function contains($collection, $value)
  172. {
  173. if ($collection instanceof IterableInterface) {
  174. return $collection->contains($value);
  175. } elseif (is_array($collection)) {
  176. return false !== array_search($value, $collection, true);
  177. }
  178. return static::any(
  179. $collection,
  180. function ($k, $v) use ($value) {
  181. return $v === $value;
  182. }
  183. );
  184. }
  185. /**
  186. * Get the keys of a collection.
  187. *
  188. * @param array|Traversable|AssociativeInterface|SequenceInterface $collection
  189. *
  190. * @return array An array containing the keys of $collection.
  191. */
  192. public static function keys($collection)
  193. {
  194. if ($collection instanceof AssociativeInterface) {
  195. return $collection->keys();
  196. } elseif ($collection instanceof SequenceInterface) {
  197. return range(0, $collection->size() - 1);
  198. } elseif (is_array($collection)) {
  199. return array_keys($collection);
  200. }
  201. $keys = array();
  202. foreach ($collection as $value) {
  203. $keys[] = $collection->key(); // https://bugs.php.net/bug.php?id=45684
  204. }
  205. return $keys;
  206. }
  207. /**
  208. * Get the values of a collection.
  209. *
  210. * @param array|Traversable|AssociativeInterface|SequenceInterface $collection
  211. *
  212. * @return array An array containing the values of $collection.
  213. */
  214. public static function values($collection)
  215. {
  216. if ($collection instanceof AssociativeInterface) {
  217. return $collection->values();
  218. } elseif ($collection instanceof SequenceInterface) {
  219. return $collection->elements();
  220. } elseif (is_array($collection)) {
  221. return array_values($collection);
  222. }
  223. $values = array();
  224. foreach ($collection as $value) {
  225. $values[] = $value;
  226. }
  227. return $values;
  228. }
  229. /**
  230. * Get the elements of a collection.
  231. *
  232. * Elements are 2-tuples of key and value (even for sequential collection types).
  233. *
  234. * @param array|Traversable|CollectionInterface $collection
  235. *
  236. * @return array An array containing the elements of $collection.
  237. */
  238. public static function elements($collection)
  239. {
  240. if ($collection instanceof AssociativeInterface) {
  241. return $collection->elements();
  242. }
  243. $elements = array();
  244. static::each(
  245. $collection,
  246. function ($key, $value) use (&$elements) {
  247. $elements[] = array($key, $value);
  248. }
  249. );
  250. return $elements;
  251. }
  252. /**
  253. * Produce a new collection by applying a transformation to each element.
  254. *
  255. * The transform must be a callable with the following signature:
  256. * function (mixed $key, mixed $value) { return array($new_key, $new_value); }
  257. *
  258. * @param array|Traversable|IterableInterface $collection
  259. * @param callable $transform The transform to apply to each element.
  260. * @param array|ArrayAccess &$result
  261. *
  262. * @return array|ArrayAccess Returns $result.
  263. */
  264. public static function map($collection, $transform, &$result = array())
  265. {
  266. static::each(
  267. $collection,
  268. function ($key, $value) use ($transform, &$result) {
  269. list($key, $value) = call_user_func($transform, $key, $value);
  270. $result[$key] = $value;
  271. }
  272. );
  273. return $result;
  274. }
  275. /**
  276. * Fetch a new collection with a subset of the elements from this collection.
  277. *
  278. * The predicate must be a callable with the following signature:
  279. * function (mixed $key, mixed $value) { return $true_to_retain_element; }
  280. *
  281. * @param array|Traversable|IterableInterface $collection
  282. * @param callable|null $predicate A predicate function used to determine which elements to include, or null to include all non-null elements.
  283. * @param array|ArrayAccess &$result
  284. *
  285. * @return array|ArrayAccess Returns $result.
  286. */
  287. public static function filter($collection, $predicate, &$result = array())
  288. {
  289. static::each(
  290. $collection,
  291. function ($key, $value) use ($predicate, &$result) {
  292. if (call_user_func($predicate, $key, $value)) {
  293. $result[$key] = $value;
  294. }
  295. }
  296. );
  297. return $result;
  298. }
  299. /**
  300. * Invokes the given callback on every element in the collection.
  301. *
  302. * This method behaves the same as {@see Collection::map()} except that the return value of the callback is not retained.
  303. *
  304. * @param array|Traversable|IterableInterface $collection
  305. * @param callable $callback The callback to invoke with each element.
  306. */
  307. public static function each($collection, $callback)
  308. {
  309. static::all(
  310. $collection,
  311. function ($key, $value) use ($callback) {
  312. call_user_func($callback, $key, $value);
  313. return true;
  314. }
  315. );
  316. }
  317. /**
  318. * Returns true if the given predicate returns true for all elements.
  319. *
  320. * The loop is short-circuited, exiting after the first element for which the predicate returns false.
  321. *
  322. * @param array|Traversable|IterableInterface $collection
  323. * @param callable $predicate
  324. *
  325. * @return boolean True if $predicate($key, $value) returns true for all elements; otherwise, false.
  326. */
  327. public static function all($collection, $predicate)
  328. {
  329. if ($collection instanceof IterableInterface) {
  330. // Wrap the callback such that a sequential index is produced for the first argument ...
  331. if ($collection instanceof SequenceInterface) {
  332. $index = 0;
  333. $original = $predicate;
  334. $predicate = function ($value) use (&$index, $original) {
  335. return call_user_func($original, $index++, $value);
  336. };
  337. }
  338. return $collection->all($predicate);
  339. } elseif (is_array($collection)) {
  340. foreach ($collection as $key => $value) {
  341. if (!call_user_func($predicate, $key, $value)) {
  342. return false;
  343. }
  344. }
  345. } else {
  346. foreach ($collection as $value) {
  347. $key = $collection->key(); // https://bugs.php.net/bug.php?id=45684
  348. if (!call_user_func($predicate, $key, $value)) {
  349. return false;
  350. }
  351. }
  352. }
  353. return true;
  354. }
  355. /**
  356. * Returns true if the given predicate returns true for any element.
  357. *
  358. * The loop is short-circuited, exiting after the first element for which the predicate returns false.
  359. *
  360. * @param array|Traversable|IterableInterface $collection
  361. * @param callable $predicate
  362. *
  363. * @return boolean True if $predicate($key, $value) returns true for any element; otherwise, false.
  364. */
  365. public static function any($collection, $predicate)
  366. {
  367. return !static::all(
  368. $collection,
  369. function ($key, $value) use ($predicate) {
  370. return !call_user_func($predicate, $key, $value);
  371. }
  372. );
  373. }
  374. /**
  375. * Check if a collection contains sequential integer keys.
  376. *
  377. * @param array|Traversable|CollectionInterface $collection
  378. *
  379. * @return boolean True if the collection contains sequential integer keys; otherwise, false.
  380. */
  381. public static function isSequential($collection)
  382. {
  383. if ($collection instanceof CollectionInterface) {
  384. return $collection instanceof SequenceInterface;
  385. }
  386. $expectedKey = 0;
  387. return static::all(
  388. $collection,
  389. function ($key, $value) use (&$expectedKey) {
  390. return $key === $expectedKey++;
  391. }
  392. );
  393. }
  394. /**
  395. * Get an iterator for any traversable type.
  396. *
  397. * @param mixed<mixed> $collection
  398. *
  399. * @return Iterator
  400. * @throws InvalidArgumentException if no iterator can be produced from the given collection.
  401. */
  402. public static function getIterator($collection)
  403. {
  404. if (is_array($collection)) {
  405. $collection = new ArrayIterator($collection);
  406. } else {
  407. while ($collection instanceof IteratorAggregate) {
  408. $collection = $collection->getIterator();
  409. }
  410. }
  411. if (!$collection instanceof Iterator) {
  412. throw new InvalidArgumentException(
  413. 'Could not produce an iterator for ' . Repr::repr($collection) . '.'
  414. );
  415. }
  416. return $collection;
  417. }
  418. /**
  419. * Add an element to a collection.
  420. *
  421. * @param MutableSequenceInterface|QueuedAccessInterface|SetInterface|ArrayAccess|array &$collection The collection to add to.
  422. * @param mixed $element The element to add.
  423. */
  424. public static function addElement(&$collection, $element)
  425. {
  426. if ($collection instanceof MutableSequenceInterface) {
  427. $collection->pushBack($element);
  428. } elseif ($collection instanceof QueuedAccessInterface) {
  429. $collection->push($element);
  430. } elseif ($collection instanceof SetInterface) {
  431. $collection->add($element);
  432. } elseif ($collection instanceof ArrayAccess) {
  433. $collection[] = $element;
  434. } else {
  435. $collection[] = $element;
  436. }
  437. }
  438. /**
  439. * Add elements from one collection to another.
  440. *
  441. * @param MutableSequenceInterface|QueuedAccessInterface|SetInterface|ArrayAccess|array &$collection The collection to append to.
  442. * @param mixed<mixed> $elements The elements to be appended.
  443. */
  444. public static function addElements(&$collection, $elements)
  445. {
  446. foreach ($elements as $element) {
  447. static::addElement($collection, $element);
  448. }
  449. }
  450. /**
  451. * Create a string by joining the elements in a collection.
  452. *
  453. * @param string $separator The separator string to place between elements.
  454. * @param mixed<mixed> $collection The collection to join.
  455. * @param string $emptyResult The result to return when there are no elements in the collection.
  456. * @param callable|null $transform The transform to apply to convert each element to a string.
  457. *
  458. * @return string A string containing each element in $collection, transformed by $transform, separated by $separator.
  459. */
  460. public static function implode(
  461. $separator,
  462. $collection,
  463. $emptyResult = '',
  464. $transform = null
  465. ) {
  466. // Create an identity transform if none is provided ...
  467. if (null === $transform) {
  468. $transform = function ($element) {
  469. return $element;
  470. };
  471. }
  472. $iterator = static::getIterator($collection);
  473. $iterator->rewind();
  474. if (!$iterator->valid()) {
  475. return $emptyResult;
  476. }
  477. $result = call_user_func($transform, $iterator->current());
  478. $iterator->next();
  479. while ($iterator->valid()) {
  480. $result .= $separator . call_user_func($transform, $iterator->current());
  481. $iterator->next();
  482. }
  483. return $result;
  484. }
  485. /**
  486. * Split a string based on a separator.
  487. *
  488. * @param string $separator The separator string to place between elements.
  489. * @param string $string The string to split.
  490. * @param integer|null $limit The maximum number of elements to insert into $collection, or null for unlimited.
  491. * @param MutableSequenceInterface|QueuedAccessInterface|SetInterface|ArrayAccess|array &$collection The collection to append to, can be any type supported by {@see Collection::addElement()}.
  492. * @param callable|null $transform The transform to apply to each element before insertion into $collection.
  493. * @param string|null $encoding The string encoding to use, or null to use the internal encoding ({@see mb_internal_encoding()}).
  494. *
  495. * @return mixed<mixed> Returns $collection.
  496. */
  497. public static function explode(
  498. $separator,
  499. $string,
  500. $limit = null,
  501. &$collection = array(),
  502. $transform = null,
  503. $encoding = null
  504. ) {
  505. // Attempt to auto-detect encoding from $string ...
  506. if (null === $encoding) {
  507. $encoding = mb_internal_encoding();
  508. }
  509. // Create an identity transform if none is provided ...
  510. if (null === $transform) {
  511. $transform = function ($element) {
  512. return $element;
  513. };
  514. }
  515. $separatorLength = mb_strlen($separator, $encoding);
  516. $stringLength = mb_strlen($string, $encoding);
  517. $count = 0;
  518. $begin = 0;
  519. $end = 0;
  520. while ($end < $stringLength) {
  521. if ($limit !== null && ++$count >= $limit) {
  522. $end = $stringLength;
  523. } else {
  524. $end = mb_strpos($string, $separator, $begin, $encoding);
  525. if (false === $end) {
  526. $end = $stringLength;
  527. }
  528. }
  529. $element = mb_substr(
  530. $string,
  531. $begin,
  532. $end - $begin,
  533. $encoding
  534. );
  535. static::addElement(
  536. $collection,
  537. call_user_func($transform, $element)
  538. );
  539. $begin = $end + $separatorLength;
  540. }
  541. return $collection;
  542. }
  543. /**
  544. * Fetch the traits for an iterator.
  545. *
  546. * @param array|Traversable|Countable|TraitsProviderInterface $iterator
  547. *
  548. * @return Traits
  549. */
  550. public static function iteratorTraits($iterator)
  551. {
  552. if ($iterator instanceof TraitsProviderInterface) {
  553. return $iterator->iteratorTraits();
  554. } elseif (is_array($iterator)) {
  555. return new Traits(true, true);
  556. } elseif ($iterator instanceof SplDoublyLinkedList) {
  557. return new Traits(true, true);
  558. } elseif ($iterator instanceof SplHeap) {
  559. return new Traits(true, true);
  560. } elseif ($iterator instanceof SplPriorityQueue) {
  561. return new Traits(true, true);
  562. } elseif ($iterator instanceof SplFixedArray) {
  563. return new Traits(true, true);
  564. } elseif ($iterator instanceof SplObjectStorage) {
  565. return new Traits(true, true);
  566. } else {
  567. return new Traits($iterator instanceof Countable, false);
  568. }
  569. }
  570. /**
  571. * Compare two collections.
  572. *
  573. * @param mixed<mixed> $lhs
  574. * @param mixed<mixed> $rhs
  575. * @param callable $comparator The comparator to use for size and element comparisons.
  576. *
  577. * @return integer The result of the comparison.
  578. */
  579. public static function compare($lhs, $rhs, $comparator = 'Icecave\Parity\Parity::compare')
  580. {
  581. $lhsSize = static::trySize($lhs);
  582. $rhsSize = static::trySize($rhs);
  583. if ($lhsSize !== $rhsSize && null !== $lhsSize && null !== $rhsSize) {
  584. return call_user_func($comparator, $lhsSize, $rhsSize);
  585. }
  586. $lhs = static::getIterator($lhs);
  587. $rhs = static::getIterator($rhs);
  588. $lhs->rewind();
  589. $rhs->rewind();
  590. while ($lhs->valid() && $rhs->valid()) {
  591. $cmp = call_user_func($comparator, $lhs->current(), $rhs->current());
  592. if (0 !== $cmp) {
  593. return $cmp;
  594. }
  595. $lhs->next();
  596. $rhs->next();
  597. }
  598. return call_user_func($comparator, $lhs->valid(), $rhs->valid());
  599. }
  600. /**
  601. * Return the index of the first element in a sorted collection that is not less than the given element.
  602. *
  603. * Searches all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  604. *
  605. * @param array|ArrayAccess $collection The sorted collection to search.
  606. * @param mixed $element The element to search for.
  607. * @param callable $comparator The comparator used to compare elements.
  608. * @param integer $begin The index at which to start the search.
  609. * @param integer|null $end The index at which to stop the search, or null to use the entire collection.
  610. */
  611. public static function lowerBound($collection, $element, $comparator, $begin = 0, $end = null)
  612. {
  613. if (null === $end) {
  614. $end = static::size($collection);
  615. }
  616. $count = $end - $begin;
  617. while ($count > 0) {
  618. $step = intval($count / 2);
  619. $pivotIndex = $begin + $step;
  620. if (call_user_func($comparator, $collection[$pivotIndex], $element) < 0) {
  621. $begin = $pivotIndex + 1;
  622. $count -= $step + 1;
  623. } else {
  624. $count = $step;
  625. }
  626. }
  627. return $begin;
  628. }
  629. /**
  630. * Return the index of the first element in a sorted collection that is greater than the given element.
  631. *
  632. * Searches all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  633. *
  634. * @param array|ArrayAccess $collection The sorted collection to search.
  635. * @param mixed $element The element to search for.
  636. * @param callable $comparator The comparator used to compare elements.
  637. * @param integer $begin The index at which to start the search.
  638. * @param integer|null $end The index at which to stop the search, or null to use the entire collection.
  639. */
  640. public static function upperBound($collection, $element, $comparator, $begin = 0, $end = null)
  641. {
  642. if (null === $end) {
  643. $end = static::size($collection);
  644. }
  645. $count = $end - $begin;
  646. while ($count > 0) {
  647. $step = intval($count / 2);
  648. $pivotIndex = $begin + $step;
  649. if (call_user_func($comparator, $collection[$pivotIndex], $element) <= 0) {
  650. $begin = $pivotIndex + 1;
  651. $count -= $step + 1;
  652. } else {
  653. $count = $step;
  654. }
  655. }
  656. return $begin;
  657. }
  658. /**
  659. * Perform a binary search on a sorted sequence.
  660. *
  661. * Searches all elements in the range [$begin, $end), i.e. $begin is inclusive, $end is exclusive.
  662. *
  663. * @param array|ArrayAccess $collection The collection to search.
  664. * @param mixed $element The element to search for.
  665. * @param callable $comparator The comparator used to compare elements.
  666. * @param integer $begin The index at which to start the search.
  667. * @param integer|null $end The index at which to stop the search, or null to use the entire collection.
  668. * @param integer|null &$insertIndex Assigned the index at which $element must be inserted to maintain sortedness.
  669. *
  670. * @return integer|null The index at which an element equal to $element is present in $collection.
  671. */
  672. public static function binarySearch($collection, $element, $comparator, $begin = 0, $end = null, &$insertIndex = null)
  673. {
  674. if (null === $end) {
  675. $end = static::size($collection);
  676. }
  677. $insertIndex = static::lowerBound($collection, $element, $comparator, $begin, $end);
  678. if ($insertIndex === $end) {
  679. return null;
  680. } elseif (0 !== call_user_func($comparator, $collection[$insertIndex], $element)) {
  681. return null;
  682. }
  683. return $insertIndex;
  684. }
  685. }