diff --git a/composer.json b/composer.json index 6870327..5f2e011 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "Collections library for php language", "minimum-stability": "dev", "license": "MIT", - "version": "1.1.1", + "version": "1.1.2", "authors": [ { "name": "Maxim Sokolovsky", diff --git a/src/WS/Utils/Collections/CollectionFactory.php b/src/WS/Utils/Collections/CollectionFactory.php index 19206cd..a4fb7e5 100644 --- a/src/WS/Utils/Collections/CollectionFactory.php +++ b/src/WS/Utils/Collections/CollectionFactory.php @@ -5,7 +5,10 @@ namespace WS\Utils\Collections; +use Iterator; +use IteratorAggregate; use RuntimeException; +use WS\Utils\Collections\Exception\UnsupportedException; class CollectionFactory { @@ -37,7 +40,7 @@ public static function generate(int $times, ?callable $generator = null): Collec /** * Generate collection of int numbers between $from and $to. If $to arg is absent $from - is count of numbers * @param int $from - * @param int $to + * @param int|null $to * @return Collection */ public static function numbers(int $from, ?int $to = null): Collection @@ -68,8 +71,21 @@ public static function fromStrict(array $values): Collection return new ArrayStrictList($values); } + /** + * @throws UnsupportedException + */ public static function fromIterable(iterable $iterable): Collection { + if (self::isStatePatternIterator($iterable)) { + if ($iterable instanceof IteratorAggregate) { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterable = $iterable->getIterator(); + } + if (!$iterable instanceof Iterator) { + throw new UnsupportedException('Only Iterator interface can be applied to IteratorCollection'); + } + return new IteratorCollection($iterable); + } $list = ArrayList::of(); foreach ($iterable as $item) { $list->add($item); @@ -82,4 +98,21 @@ public static function empty(): Collection { return ArrayList::of(); } + + private static function isStatePatternIterator(iterable $iterable): bool + { + $i = 2; + $lastItem = null; + foreach ($iterable as $item) { + if ($i === 0) { + break; + } + if (is_object($item) && $item === $lastItem) { + return true; + } + $lastItem = $item; + $i--; + } + return false; + } } diff --git a/src/WS/Utils/Collections/Exception/UnsupportedException.php b/src/WS/Utils/Collections/Exception/UnsupportedException.php new file mode 100644 index 0000000..19c866c --- /dev/null +++ b/src/WS/Utils/Collections/Exception/UnsupportedException.php @@ -0,0 +1,9 @@ +iterator = $iterator; + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function add($element): bool + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function addAll(iterable $elements): bool + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function merge(Collection $collection): bool + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function clear(): void + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function remove($element): bool + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function contains($element): bool + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function equals(Collection $collection): bool + { + throw new UnsupportedException(); + } + + public function size(): int + { + $this->iterator->rewind(); + $count = 0; + while ($this->iterator->valid()) { + $this->iterator->next(); + $count++; + } + + return $count; + } + + /** + * @codeCoverageIgnore + * @return bool + */ + public function isEmpty(): bool + { + return $this->size() === 0; + } + + public function stream(): Stream + { + return new IteratorStream($this); + } + + /** + * @codeCoverageIgnore + * @return array + */ + public function toArray(): array + { + throw new UnsupportedException(); + } + + /** + * @codeCoverageIgnore + * @return Collection + */ + public function copy(): Collection + { + throw new UnsupportedException(); + } + + public function getIterator() + { + return $this->iterator; + } +} diff --git a/src/WS/Utils/Collections/IteratorStream.php b/src/WS/Utils/Collections/IteratorStream.php new file mode 100644 index 0000000..4c02115 --- /dev/null +++ b/src/WS/Utils/Collections/IteratorStream.php @@ -0,0 +1,348 @@ +collection = $collection; + } + + public function each(callable $consumer): Stream + { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + $i = 0; + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + $item = $iterator->current(); + $consumer($item); + } + $iterator->next(); + $i++; + } + + return $this; + } + + public function walk(callable $consumer, ?int $limit = null): Stream + { + $iterationsCount = $limit ?? $this->collection->size(); + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + + $i = 0; + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + $item = $iterator->current(); + $consumerRes = $consumer($item, $i); + if ($consumerRes === false) { + break; + } + if ($i + 1 >= $iterationsCount) { + break; + } + } + + $iterator->next(); + $i++; + } + return $this; + } + + public function filter(callable $predicate): Stream + { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + $i = 0; + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + $item = $iterator->current(); + !$predicate($item) && $this->exclude($i); + } + $iterator->next(); + $i++; + } + + return $this; + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function reorganize(callable $reorganizer): Stream + { + throw new UnsupportedException(); + } + + public function allMatch(callable $predicate): bool + { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + $i = 0; + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + $current = $iterator->current(); + if (!$predicate($current)) { + return false; + } + } + $iterator->next(); + $i++; + } + return true; + } + + public function anyMatch(callable $predicate): bool + { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + $i = 0; + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + $current = $iterator->current(); + if ($predicate($current)) { + return true; + } + } + $iterator->next(); + $i++; + } + return false; + } + + /** + * @throws UnsupportedException + */ + public function map(callable $converter): Stream + { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + $i = 0; + $list = new ArrayList(); + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + $item = $iterator->current(); + $converterRes = $converter($item); + if ($converterRes === $item) { + throw new UnsupportedException('Item must be another different from sourced'); + } + $list->add($converterRes); + } + $iterator->next(); + $i++; + } + + return new SerialStream($list); + } + + /** + * @codeCoverageIgnore + */ + public function collect(callable $collector) + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function findAny() + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function findFirst(callable $filter = null) + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function findLast() + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function min(callable $comparator) + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function max(callable $comparator) + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function sort(callable $comparator): Stream + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function sortBy(callable $extractor): Stream + { + throw new UnsupportedException(); + } + + /** + * @codeCoverageIgnore + */ + public function sortDesc(callable $comparator): Stream + { + throw new UnsupportedException(); + } + + /** + * @codeCoverageIgnore + */ + public function sortByDesc(callable $extractor): Stream + { + throw new UnsupportedException(); + } + + /** + * @codeCoverageIgnore + */ + public function reverse(): Stream + { + throw new UnsupportedException(); + } + + public function reduce(callable $accumulator, $initialValue = null) + { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + $i = 0; + $accumulate = $initialValue; + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + $accumulate = $accumulator($iterator->current(), $accumulate); + } + + $i++; + $iterator->next(); + } + + return $accumulate; + } + + public function limit(int $size): Stream + { + /** @noinspection PhpUnhandledExceptionInspection */ + $iterator = $this->collection->getIterator(); + $iterator->rewind(); + $i = 0; + $countdown = $size; + while ($iterator->valid()) { + if (!$this->isExcluded($i)) { + if ($countdown <= 0) { + $this->exclude($i); + } + $countdown--; + } + $iterator->next(); + $i++; + } + + return $this; + } + + public function when(bool $condition): Stream + { + if (!$condition) { + return new DummyStreamDecorator($this); + } + + return $this; + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function getCollection(): Collection + { + throw new UnsupportedException(); + } + + public function always(): Stream + { + return $this; + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function toArray(): array + { + throw new UnsupportedException(); + } + + /** + * @throws UnsupportedException + * @codeCoverageIgnore + */ + public function getSet(): Set + { + throw new UnsupportedException(); + } + + /** + * @param int $index + * @return void + */ + private function exclude(int $index): void + { + $this->excluded[$index] = $index; + } + + private function isExcluded(int $index): bool + { + return isset($this->excluded[$index]); + } +} diff --git a/src/WS/Utils/Collections/Stream.php b/src/WS/Utils/Collections/Stream.php index 3248840..e15bad6 100644 --- a/src/WS/Utils/Collections/Stream.php +++ b/src/WS/Utils/Collections/Stream.php @@ -8,14 +8,14 @@ interface Stream { /** - * Call function for each element in collection + * Calls function for each element in collection * @param callable $consumer Function with f(mixed $element, int $index): void interface * @return Stream */ public function each(callable $consumer): Stream; /** - * Call function for $limit element in collection. If limit is null all elements will. If consumer will return false walk stop + * Call function for $limit element in collection. If limit is null all elements will. If consumer return false walk stop * @param callable $consumer Function with f(mixed $element, int $index): ?false|mixed interface. * @param int|null $limit * @return Stream @@ -57,7 +57,7 @@ public function anyMatch(callable $predicate): bool; public function map(callable $converter): Stream; /** - * Call collector function for collection. It is terminate function + * Call collector function for collection. It is terminated function * @param callable $collector Function f(Collection $c): mixed * @return mixed */ @@ -130,7 +130,7 @@ public function reverse(): Stream; /** * Reduce collection to single value with accumulator - * @param callable $accumulator + * @param callable $accumulator * @param mixed|null $initialValue * @return mixed */ diff --git a/tests/WS/Utils/Collections/Iterator/IteratorHandlingTest.php b/tests/WS/Utils/Collections/Iterator/IteratorHandlingTest.php new file mode 100644 index 0000000..15fa58d --- /dev/null +++ b/tests/WS/Utils/Collections/Iterator/IteratorHandlingTest.php @@ -0,0 +1,341 @@ +stream() + ->each(static function ($i) use ($intGenerator) { + self::assertEquals($intGenerator()->getValue(), $i); + }) + ; + } + + /** + * @test + */ + public function checkPhpDirectoryIterator() + { + $iterable = new DirectoryIterator(__DIR__); + $files = CollectionFactory::fromIterable($iterable) + ->stream() + ->map(static function (DirectoryIterator $current) { + return $current->getBasename(); + }) + ->filter(Predicates::lockDuplicated()) + ->toArray() + ; + self::assertTrue(count($files) > 3); + } + + /** + * @test + */ + public function checkCustomIterator() + { + $iterable = new StatePatternIterator(5); + $differenceCount = CollectionFactory::fromIterable($iterable) + ->stream() + ->map(static function (ValueKeeper $valueKeeper) { + return $valueKeeper->getValue(); + }) + ->filter(Predicates::lockDuplicated()) + ->getCollection() + ->size() + ; + self::assertEquals(5, $differenceCount); + } + + /** + * @test + */ + public function checkStateIteratorFilter() + { + $iterable = new StatePatternIterator(6); + $result = CollectionFactory::fromIterable($iterable) + ->stream() + ->filter(static function (ValueKeeper $valueKeeper) { + return $valueKeeper->getValue() <= 3; + }) + ->map(static function (ValueKeeper $valueKeeper) { + return $valueKeeper->getValue(); + }) + ->toArray() + ; + self::assertEquals([0, 1, 2, 3], $result); + } + + /** + * @test + */ + public function checkSizeCutting() + { + $iterable = new StatePatternIterator(6); + $result = CollectionFactory::fromIterable($iterable) + ->stream() + ->filter(static function (ValueKeeper $valueKeeper) { + return $valueKeeper->getValue() > 0; + }) + ->limit(2) + ->map(static function (ValueKeeper $valueKeeper) { + return $valueKeeper->getValue(); + }) + ->toArray() + ; + self::assertEquals([1, 2], $result); + } + + /** + * @test + */ + public function checkRightSizeInStateIterator() + { + $iterable = new StatePatternIterator(3); + $size = CollectionFactory::fromIterable($iterable)->size(); + self::assertEquals(3, $size); + } + + /** + * @test + */ + public function checkEmptyIterator() + { + $iterable = new StatePatternIterator(0); + self::assertTrue(CollectionFactory::fromIterable($iterable)->isEmpty()); + } + + /** + * @test + */ + public function checkEachBehavior() + { + $iterable = new StatePatternIterator(6); + $i = 1; + CollectionFactory::fromIterable($iterable) + ->stream() + ->filter(static function (ValueKeeper $valueKeeper) { + return $valueKeeper->getValue() > 0; + }) + ->limit(4) + ->each(static function (ValueKeeper $valueKeeper) use (& $i) { + self::assertEquals($i++, $valueKeeper->getValue()); + }) + ; + } + + /** + * @test + */ + public function checkWalkingByStateIterator() + { + $iterable = new StatePatternIterator(6); + $i = 2; + $result = CollectionFactory::fromIterable($iterable) + ->stream() + ->filter(static function (ValueKeeper $keeper) { + return $keeper->getValue() > 1; + }) + ->walk(static function (ValueKeeper $keeper) use (& $i) { + self::assertTrue($keeper->getValue() === $i); + $i++; + }, 2) + ->map(static function (ValueKeeper $keeper) { + return $keeper->getValue(); + }) + ->toArray() + ; + self::assertEquals([2, 3, 4, 5], $result); + } + + /** + * @test + */ + public function checkWalkingWithStopping() + { + $iterable = new StatePatternIterator(6); + $i = 2; + $result = CollectionFactory::fromIterable($iterable) + ->stream() + ->walk(static function () use (& $i) { + if ($i <= 0) { + return false; + } + $i--; + return true; + }) + ->map(static function (ValueKeeper $keeper) { + return $keeper->getValue(); + }) + ->getCollection() + ; + self::assertEquals(6, $result->size()); + self::assertEquals(0, $i); + } + + /** + * @test + */ + public function withTwoElementsChecking() + { + $iterable = new StatePatternIterator(2); + $collection = CollectionFactory::fromIterable($iterable); + + self::assertInstanceOf(IteratorCollection::class, $collection); + self::assertInstanceOf(IteratorStream::class, $collection->stream()); + } + + /** + * @test + */ + public function checkReduceMethod() + { + $iterator = new StatePatternIterator(5); + $sumOfThree = CollectionFactory::fromIterable($iterator) + ->stream() + ->filter(static function (ValueKeeper $keeper) { + return $keeper->getValue() > 1; + }) + ->reduce(static function (ValueKeeper $keeper, $sum) { + return $sum + $keeper->getValue(); + }, 0) + ; + self::assertEquals(9, $sumOfThree); + } + + /** + * @test + */ + public function allMatchIteratorChecking() + { + $iterator = new StatePatternIterator(5); + $collection = CollectionFactory::fromIterable($iterator); + + $everythingIsInt = $collection + ->stream() + ->allMatch(static function (ValueKeeper $keeper) { + return is_int($keeper->getValue()); + }) + ; + + $greatThanTwoPredicate = static function (ValueKeeper $keeper) { + return $keeper->getValue() > 2; + }; + $everythingIsGreatThanTwo = $collection + ->stream() + ->allMatch($greatThanTwoPredicate) + ; + + $everythingIsGreatThanTwoWithFilter = $collection + ->stream() + ->filter($greatThanTwoPredicate) + ->allMatch($greatThanTwoPredicate) + ; + + self::assertTrue($everythingIsInt); + self::assertFalse($everythingIsGreatThanTwo); + self::assertTrue($everythingIsGreatThanTwoWithFilter); + } + + /** + * @test + */ + public function anyMatchIteratorChecking() + { + $greatThanTwoPredicate = static function (ValueKeeper $keeper) { + return $keeper->getValue() > 2; + }; + $greatThanTenPredicate = static function (ValueKeeper $keeper) { + return $keeper->getValue() > 10; + }; + + $iterator = new StatePatternIterator(5); + $collection = CollectionFactory::fromIterable($iterator); + + $hasElementsWithGreatThanWho = $collection + ->stream() + ->anyMatch($greatThanTwoPredicate) + ; + + $hasElementsWithGreatThanTen = $collection + ->stream() + ->anyMatch($greatThanTenPredicate) + ; + + $hasLessThanTwoFiltered = $collection + ->stream() + ->filter($greatThanTwoPredicate) + ->anyMatch(static function (ValueKeeper $keeper) { + return $keeper->getValue() <= 2; + }) + ; + + self::assertTrue($hasElementsWithGreatThanWho); + self::assertFalse($hasElementsWithGreatThanTen); + self::assertFalse($hasLessThanTwoFiltered); + } + + /** + * @test + */ + public function cunningMapChecking() + { + $iterator = new StatePatternIterator(3); + self::expectException(UnsupportedException::class); + CollectionFactory::fromIterable($iterator) + ->stream() + ->map(static function ($item) { + return $item; + }) + ->getCollection() + ; + } + + /** + * @test + */ + public function iterableStreamFlowChecking() + { + $greatThanTwoPredicate = static function (ValueKeeper $keeper) { + return $keeper->getValue() > 2; + }; + + $iterator = new StatePatternIterator(5); + + $array = CollectionFactory::fromIterable($iterator) + ->stream() + ->when(false) + ->filter($greatThanTwoPredicate) + ->always() + ->map(static function (ValueKeeper $keeper) { + return $keeper->getValue(); + }) + ->toArray() + ; + self::assertCount(5, $array); + + $stream = CollectionFactory::fromIterable($iterator) + ->stream() + ->when(true) + ->always() + ; + self::assertInstanceOf(IteratorStream::class, $stream); + } +} + diff --git a/tests/WS/Utils/Collections/Iterator/StatePatternIterator.php b/tests/WS/Utils/Collections/Iterator/StatePatternIterator.php new file mode 100644 index 0000000..c02f65c --- /dev/null +++ b/tests/WS/Utils/Collections/Iterator/StatePatternIterator.php @@ -0,0 +1,63 @@ +count = $count; + } + + public function getIterator() + { + return new class($this->count) implements Iterator, ValueKeeper { + private $current; + private $count; + + public function __construct(int $count) + { + $this->count = $count; + $this->rewind(); + } + + public function current() + { + return $this; + } + + public function next() + { + $this->current++; + } + + public function key() + { + return $this->current; + } + + public function valid(): bool + { + return $this->current < $this->count; + } + + public function rewind() + { + $this->current = 0; + } + + public function getValue() + { + return $this->current; + } + }; + } +} diff --git a/tests/WS/Utils/Collections/Iterator/ValueKeeper.php b/tests/WS/Utils/Collections/Iterator/ValueKeeper.php new file mode 100644 index 0000000..a2b8964 --- /dev/null +++ b/tests/WS/Utils/Collections/Iterator/ValueKeeper.php @@ -0,0 +1,8 @@ +assertEquals($expected, $actual); } - /** @noinspection PhpUnusedParameterInspection */ /** * @dataProvider firstLastElementCases * @test @@ -565,7 +560,7 @@ public function limitedWalkCheck(): void */ public function suspendedWalkCheck(): void { - CollectionFactory::numbers(10) + $collection = CollectionFactory::numbers(10) ->stream() ->walk(function ($i) { if ($i === 2) { @@ -575,9 +570,12 @@ public function suspendedWalkCheck(): void $this->fail('El this index > 2 should not be here'); } return null; - }); + }) + ->getCollection() + ; $this->assertTrue(true); + $this->assertEquals(10, $collection->size()); } /**