├── .gitignore ├── src └── WS │ └── Utils │ └── Collections │ ├── HashCodeAware.php │ ├── Set.php │ ├── CollectionConstructorTrait.php │ ├── IndexIterable.php │ ├── Functions │ ├── Group │ │ ├── Aggregator │ │ │ ├── Aggregator.php │ │ │ ├── Count.php │ │ │ ├── Last.php │ │ │ ├── First.php │ │ │ ├── Sum.php │ │ │ ├── AddToSet.php │ │ │ ├── AbstractFieldAggregator.php │ │ │ ├── Max.php │ │ │ ├── Min.php │ │ │ └── Avg.php │ │ └── Group.php │ ├── Consumers.php │ ├── Expression │ │ ├── Operator │ │ │ ├── OrOperator.php │ │ │ ├── AndOperator.php │ │ │ └── AbstractOperator.php │ │ └── BoolExpression.php │ ├── Converters.php │ ├── ObjectFunctions.php │ ├── Comparators.php │ ├── Collectors.php │ ├── Reorganizers.php │ └── Predicates.php │ ├── Iterator │ ├── Iterator.php │ ├── IterateResult.php │ ├── IteratorFactory.php │ └── CallbackIterator.php │ ├── Stack.php │ ├── MapEntry.php │ ├── Queue.php │ ├── MapFactory.php │ ├── ArrayStrictList.php │ ├── ListSequence.php │ ├── Map.php │ ├── ArrayQueue.php │ ├── RemoveTraverseTrait.php │ ├── ArrayStack.php │ ├── Collection.php │ ├── CollectionFactory.php │ ├── HashSet.php │ ├── ArrayList.php │ ├── AbstractCollection.php │ ├── ImmutableList.php │ ├── DummyStreamDecorator.php │ ├── HashMap.php │ ├── Stream.php │ └── SerialStream.php ├── tests └── WS │ └── Utils │ └── Collections │ ├── UnitConstraints │ ├── StaticCompareCreation.php │ ├── CollectionIsEqual.php │ ├── CollectionIsNotEqual.php │ ├── CollectionContainsSameElements.php │ └── CollectionComparingConstraint.php │ ├── HashMapTest.php │ ├── Utils │ ├── CollectionAwareTrait.php │ ├── ExampleObject.php │ ├── InvokeCounter.php │ └── TestInteger.php │ ├── HashSetTest.php │ ├── ConsumersFunctionsTest.php │ ├── ListInterfaceTestTrait.php │ ├── Iterator │ ├── IntGeneratorCallback.php │ └── CallbackIteratorTest.php │ ├── PredicateFunctionTest.php │ ├── ObjectsFunctionsTest.php │ ├── Functions │ ├── StrImplodeAggregatorTest.php │ ├── Expression │ │ └── BoolExpressionTest.php │ ├── ShuffleReorganizerTest.php │ ├── RandomReorganizerTest.php │ └── Group │ │ └── Aggregator │ │ ├── CountTest.php │ │ ├── FirstTest.php │ │ ├── AddToSetTest.php │ │ ├── LastTest.php │ │ ├── MaxTest.php │ │ ├── MinTest.php │ │ ├── SumTest.php │ │ └── AvgTest.php │ ├── MapFactoryTest.php │ ├── AggregatorsFunctionsTest.php │ ├── ImmutableStreamTest.php │ ├── ConvertersFunctionsTest.php │ ├── CollectionFactoryTest.php │ ├── ImmutableListTest.php │ ├── ArrayStrictListTest.php │ ├── ArrayQueueTest.php │ ├── AggregateGroupingTest.php │ ├── ReorganizersFunctionsTest.php │ ├── SetInterfaceTestTrait.php │ ├── ArrayListTest.php │ ├── ArrayStackTest.php │ ├── ComparatorsTest.php │ ├── ConditionsStreamTest.php │ ├── MapInterfaceTestTrait.php │ ├── CollectionInterfaceTestTrait.php │ └── PredicatesFunctionsTest.php ├── phpunit.xml ├── LICENSE ├── composer.json └── .github └── workflows └── php.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | vendor 3 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/HashCodeAware.php: -------------------------------------------------------------------------------- 1 | 4 | * @license MIT 5 | */ 6 | 7 | namespace WS\Utils\Collections; 8 | 9 | interface Set extends Collection 10 | { 11 | 12 | } 13 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/UnitConstraints/StaticCompareCreation.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | interface Aggregator 11 | { 12 | public function __invoke(Collection $collection); 13 | } 14 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/HashMapTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | class Count implements Aggregator 11 | { 12 | 13 | public function __invoke(Collection $collection) 14 | { 15 | return $collection->size(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/HashSetTest.php: -------------------------------------------------------------------------------- 1 | that dumps element 12 | * @return callable 13 | */ 14 | public static function dump(): callable 15 | { 16 | return static function ($el) { 17 | var_dump($el); 18 | }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/UnitConstraints/CollectionIsEqual.php: -------------------------------------------------------------------------------- 1 | equals($other); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/UnitConstraints/CollectionIsNotEqual.php: -------------------------------------------------------------------------------- 1 | equals($other); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Iterator/Iterator.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Expression\Operator; 7 | 8 | class OrOperator extends AbstractOperator 9 | { 10 | 11 | public function __invoke(bool $operand, $item): bool 12 | { 13 | if ($operand) { 14 | return true; 15 | } 16 | $checker = $this->checker; 17 | return $checker($item); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Expression/Operator/AndOperator.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Expression\Operator; 7 | 8 | class AndOperator extends AbstractOperator 9 | { 10 | 11 | public function __invoke(bool $operand, $item): bool 12 | { 13 | if (!$operand) { 14 | return false; 15 | } 16 | $checker = $this->checker; 17 | return $checker($item); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Aggregator/Last.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | class Last extends AbstractFieldAggregator implements Aggregator 11 | { 12 | 13 | public function __invoke(Collection $collection) 14 | { 15 | if (!$last = $collection->stream()->findLast()) { 16 | return null; 17 | } 18 | return $this->getValue($last); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Aggregator/First.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | class First extends AbstractFieldAggregator implements Aggregator 11 | { 12 | 13 | public function __invoke(Collection $collection) 14 | { 15 | if (!$first = $collection->stream()->findFirst()) { 16 | return null; 17 | } 18 | 19 | return $this->getValue($first); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Aggregator/Sum.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | class Sum extends AbstractFieldAggregator implements Aggregator 11 | { 12 | 13 | public function __invoke(Collection $collection) 14 | { 15 | $sum = 0; 16 | foreach ($collection as $element) { 17 | $sum += $this->getValue($element); 18 | } 19 | return $sum; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Expression/Operator/AbstractOperator.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Expression\Operator; 7 | 8 | abstract class AbstractOperator 9 | { 10 | protected $checker; 11 | 12 | public function __construct(callable $checker) 13 | { 14 | $this->checker = $checker; 15 | } 16 | 17 | public function getChecker(): callable 18 | { 19 | return $this->checker; 20 | } 21 | 22 | abstract public function __invoke(bool $operand, $item): bool; 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Stack.php: -------------------------------------------------------------------------------- 1 | key = $key; 16 | $this->value = $value; 17 | } 18 | 19 | /** 20 | * @return mixed 21 | */ 22 | public function getKey() 23 | { 24 | return $this->key; 25 | } 26 | 27 | /** 28 | * @return mixed 29 | */ 30 | public function getValue() 31 | { 32 | return $this->value; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Aggregator/AddToSet.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | use WS\Utils\Collections\HashSet; 10 | 11 | class AddToSet extends AbstractFieldAggregator implements Aggregator 12 | { 13 | 14 | public function __invoke(Collection $collection) 15 | { 16 | $set = new HashSet(); 17 | foreach ($collection as $element) { 18 | $set->add($this->getValue($element)); 19 | } 20 | return $set->toArray(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Aggregator/AbstractFieldAggregator.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Functions\ObjectFunctions; 9 | 10 | abstract class AbstractFieldAggregator 11 | { 12 | 13 | private $fieldName; 14 | 15 | public function __construct(string $fieldName) 16 | { 17 | $this->fieldName = $fieldName; 18 | } 19 | 20 | protected function getValue($element) 21 | { 22 | return ObjectFunctions::getPropertyValue($element, $this->fieldName); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ConsumersFunctionsTest.php: -------------------------------------------------------------------------------- 1 | stream() 23 | ->each(Consumers::dump()) 24 | ; 25 | $str = ob_get_clean(); 26 | $this->assertNotEmpty($str); 27 | } 28 | } -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Aggregator/Max.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | class Max extends AbstractFieldAggregator implements Aggregator 11 | { 12 | 13 | public function __invoke(Collection $collection) 14 | { 15 | $max = null; 16 | foreach ($collection as $element) { 17 | $value = $this->getValue($element); 18 | if ($max === null || $max < $value) { 19 | $max = $value; 20 | } 21 | } 22 | return $max; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Aggregator/Min.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | class Min extends AbstractFieldAggregator implements Aggregator 11 | { 12 | 13 | public function __invoke(Collection $collection) 14 | { 15 | $min = null; 16 | foreach ($collection as $element) { 17 | $value = $this->getValue($element); 18 | if ($min === null || $min > $value) { 19 | $min = $value; 20 | } 21 | } 22 | return $min; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Queue.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group\Aggregator; 7 | 8 | use WS\Utils\Collections\Collection; 9 | 10 | class Avg extends AbstractFieldAggregator implements Aggregator 11 | { 12 | 13 | public function __invoke(Collection $collection) 14 | { 15 | $acc = null; 16 | $cnt = 0; 17 | foreach ($collection as $element) { 18 | $acc += $this->getValue($element); 19 | $cnt++; 20 | } 21 | if (!$cnt) { 22 | return null; 23 | } 24 | return $acc / $cnt; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Iterator/IterateResult.php: -------------------------------------------------------------------------------- 1 | value = $value; 16 | 17 | return $this; 18 | } 19 | 20 | public function setAsRunOut(): self 21 | { 22 | $this->isRunOut = true; 23 | 24 | return $this; 25 | } 26 | 27 | public function isRunOut(): bool 28 | { 29 | return $this->isRunOut; 30 | } 31 | 32 | public function getValue() 33 | { 34 | return $this->value; 35 | } 36 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ListInterfaceTestTrait.php: -------------------------------------------------------------------------------- 1 | createInstance(27, 'string', -11, 50); 18 | $iterator = $collection->getIterator(); 19 | 20 | $this->assertInstanceOf(Traversable::class, $iterator); 21 | $this->assertEquals(27, $iterator->current()); 22 | $iterator->next(); 23 | $this->assertEquals('string', $iterator->current()); 24 | $this->assertEquals(1, $iterator->key()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Utils/ExampleObject.php: -------------------------------------------------------------------------------- 1 | name = $name; 17 | 18 | return $this; 19 | } 20 | 21 | public function getName(): string 22 | { 23 | return $this->name; 24 | } 25 | 26 | public function field(): string 27 | { 28 | return $this->field; 29 | } 30 | 31 | public function setField(string $field): self 32 | { 33 | $this->field = $field; 34 | 35 | return $this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Iterator/IntGeneratorCallback.php: -------------------------------------------------------------------------------- 1 | rest = $count; 23 | } 24 | 25 | public function __invoke() 26 | { 27 | $iterateResult = new IterateResult(); 28 | if ($this->rest-- === 0) { 29 | $iterateResult->setAsRunOut(); 30 | 31 | return $iterateResult; 32 | } 33 | 34 | return $iterateResult->setValue($this->counter++); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Utils/InvokeCounter.php: -------------------------------------------------------------------------------- 1 | f = $f; 19 | } 20 | 21 | public function __invoke() 22 | { 23 | $this->calls[] = func_get_args(); 24 | if ($this->f !== null) { 25 | return call_user_func_array($this->f, func_get_args()); 26 | } 27 | return null; 28 | } 29 | 30 | public function countOfInvokes(): int 31 | { 32 | return count($this->calls); 33 | } 34 | 35 | public function calls(): array 36 | { 37 | return $this->calls; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Iterator/IteratorFactory.php: -------------------------------------------------------------------------------- 1 | setAsRunOut(); 16 | } 17 | 18 | return (new IterateResult())->setValue($current++); 19 | }); 20 | } 21 | 22 | public static function reverseSequence($length): Iterator 23 | { 24 | $current = $length - 1; 25 | return new CallbackIterator(static function () use (& $current) { 26 | if ($current === -1) { 27 | return (new IterateResult())->setAsRunOut(); 28 | } 29 | 30 | return (new IterateResult())->setValue($current--); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Utils/TestInteger.php: -------------------------------------------------------------------------------- 1 | value = $value; 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getHashCode(): string 35 | { 36 | $hash = 7; 37 | $hash = 31 * $hash + $this->value; 38 | return $hash.''; 39 | } 40 | 41 | /** 42 | * @return int 43 | */ 44 | public function getValue(): int 45 | { 46 | return $this->value; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/PredicateFunctionTest.php: -------------------------------------------------------------------------------- 1 | assertTrue($f($i)); 26 | } 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function notResistanceIntegratedChecking(): void 33 | { 34 | $actualSize = CollectionFactory::generate(10) 35 | ->stream() 36 | ->filter(Predicates::notResistance()) 37 | ->getCollection() 38 | ->size(); 39 | 40 | $this->assertEquals(10, $actualSize); 41 | } 42 | } -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ObjectsFunctionsTest.php: -------------------------------------------------------------------------------- 1 | property = 'value'; 22 | $o->setName('nameValue'); 23 | $o->setField('fieldValue'); 24 | 25 | $this->assertEquals('value', ObjectFunctions::getPropertyValue($o, 'property')); 26 | $this->assertEquals('nameValue', ObjectFunctions::getPropertyValue($o, 'name')); 27 | $this->assertEquals('fieldValue', ObjectFunctions::getPropertyValue($o, 'field')); 28 | 29 | $this->expectException(RuntimeException::class); 30 | ObjectFunctions::getPropertyValue($o, 'undefinedProperty'); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 WorkSolutions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Converters.php: -------------------------------------------------------------------------------- 1 | 15 | * @param string $name 16 | * @return Closure 17 | */ 18 | public static function toPropertyValue(string $name): Closure 19 | { 20 | return static function ($obj) use ($name) { 21 | return ObjectFunctions::getPropertyValue($obj, $name); 22 | }; 23 | } 24 | 25 | /** 26 | * Returns function that returns assoc array ['fieldName1' => 'value', 'fieldName2' => 'value2'] 27 | * @param array $names 28 | * @return Closure 29 | */ 30 | public static function toProperties(array $names): Closure 31 | { 32 | return static function ($obj) use ($names) { 33 | $res = []; 34 | foreach ($names as $name) { 35 | $res[$name] = ObjectFunctions::getPropertyValue($obj, $name); 36 | } 37 | 38 | return $res; 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/MapFactory.php: -------------------------------------------------------------------------------- 1 | $value) { 28 | $map->put($key, $value); 29 | } 30 | 31 | return $map; 32 | } 33 | 34 | /** 35 | * Creates empty map object 36 | * @return Map 37 | */ 38 | public static function emptyObject(): Map 39 | { 40 | return self::newObject(); 41 | } 42 | 43 | /** 44 | * Creates empty Map object 45 | * @return Map 46 | */ 47 | private static function newObject(): Map 48 | { 49 | return new HashMap(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/StrImplodeAggregatorTest.php: -------------------------------------------------------------------------------- 1 | assertIsCallable($f); 23 | } 24 | 25 | public function implodeCases(): array 26 | { 27 | return [ 28 | [[1, 2, 3], ', ', '1, 2, 3'], 29 | [[], '1111', ''], 30 | [[1], '|', '1'], 31 | [null, '', ''] 32 | ]; 33 | } 34 | 35 | /** 36 | * @dataProvider implodeCases 37 | * @test 38 | * @param $data 39 | * @param $glue 40 | * @param $result 41 | */ 42 | public function imploding($data, $glue, $result): void 43 | { 44 | $f = Collectors::concat($glue); 45 | $this->assertSame($f($this->toCollection($data)), $result); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Iterator/CallbackIteratorTest.php: -------------------------------------------------------------------------------- 1 | next(); 20 | $one = $i->next(); 21 | $this->assertEquals(1, $one); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function hasNextChecking(): void 28 | { 29 | $i = new CallbackIterator(new IntGeneratorCallback(2)); 30 | $i->next(); 31 | $i->next(); 32 | 33 | $this->assertFalse($i->hasNext()); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function getOverflowException(): void 40 | { 41 | $i = new CallbackIterator(new IntGeneratorCallback(2)); 42 | $i->next(); 43 | $i->next(); 44 | 45 | $this->expectException(RuntimeException::class); 46 | 47 | $i->next(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "worksolutions/php-collections", 3 | "description": "Collections library for php language", 4 | "minimum-stability": "dev", 5 | "license": "MIT", 6 | "version": "1.0.10", 7 | "authors": [ 8 | { 9 | "name": "Maxim Sokolovsky", 10 | "email": "sokolovsky@worksolutions.ru" 11 | }, 12 | { 13 | "name": "Anton Lytkin", 14 | "email": "a.lytkin@worksolutions.ru" 15 | }, 16 | { 17 | "name": "Igor Pomiluyko", 18 | "email": "pomiluyko@worksolutions.ru" 19 | } 20 | ], 21 | "require": { 22 | "php": ">=7.1", 23 | "ext-json": "*" 24 | }, 25 | "require-dev": { 26 | "phpunit/phpunit": "^7.5" 27 | }, 28 | "suggest": { 29 | "phpbench/phpbench": "Uses only for benchmark purposes" 30 | }, 31 | "autoload": { 32 | "psr-4": {"WS\\Utils\\Collections\\": "src/WS/Utils/Collections"} 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "WS\\Utils\\Collections\\": "tests/WS/Utils/Collections", 37 | "Benchmarks\\": "benchmarks" 38 | } 39 | }, 40 | "scripts": { 41 | "test": [ 42 | "phpunit" 43 | ], 44 | "test-with-coverage": [ 45 | "phpunit --coverage-clover coverage.xml" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Iterator/CallbackIterator.php: -------------------------------------------------------------------------------- 1 | generator = $f; 25 | } 26 | 27 | public function next() 28 | { 29 | if (!$this->hasNext()) { 30 | throw new RuntimeException('Iterated element is absent'); 31 | } 32 | $value = $this->currentIterateResult->getValue(); 33 | $this->currentIterateResult = null; 34 | return $value; 35 | } 36 | 37 | public function hasNext(): bool 38 | { 39 | if ($this->currentIterateResult === null) { 40 | $f = $this->generator; 41 | $this->currentIterateResult = $f(); 42 | } 43 | return $this->currentIterateResult->isRunOut() === false; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/ArrayStrictList.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections; 7 | 8 | use InvalidArgumentException; 9 | 10 | class ArrayStrictList extends ArrayList 11 | { 12 | protected function afterElementsSet(): void 13 | { 14 | foreach ($this->elements as $element) { 15 | $this->afterElementAdd($element); 16 | } 17 | parent::afterElementsSet(); 18 | } 19 | 20 | protected function afterElementAdd($element): void 21 | { 22 | $firstElement = $this->elements[0]; 23 | if (null === $firstElement && !count($this->elements)) { 24 | return; 25 | } 26 | if (is_object($firstElement)) { 27 | if (!is_object($element) || (get_class($firstElement) !== get_class($element))) { 28 | throw new InvalidArgumentException('Collection must contain elements with identical types'); 29 | } 30 | return; 31 | } 32 | if (is_object($element) || (gettype($firstElement) !== gettype($element))) { 33 | throw new InvalidArgumentException('Collection must contain elements with identical types'); 34 | } 35 | parent::afterElementAdd($element); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/ObjectFunctions.php: -------------------------------------------------------------------------------- 1 | isPublic()) { 27 | return $object->{$fieldName}; 28 | } 29 | 30 | if (isset($object->{$fieldName})) { 31 | return $object->{$fieldName}; 32 | } 33 | 34 | if (method_exists($object, $fieldName)) { 35 | return $object->{$fieldName}(); 36 | } 37 | 38 | if (method_exists($object, 'get'.$fieldName)) { 39 | return $object->{'get'.$fieldName}(); 40 | } 41 | throw new RuntimeException("Field $fieldName is not exist for object ".var_export($object, true)); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/MapFactoryTest.php: -------------------------------------------------------------------------------- 1 | size()); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function mapFromIterableIsCreated(): void 28 | { 29 | $arrayIterator = new ArrayIterator(['a' => 'A', 'b' => 'B']); 30 | $map = MapFactory::fromIterable($arrayIterator); 31 | self::assertEquals(2, $map->size()); 32 | self::assertEquals(['a', 'b'], $map->keys()->toArray()); 33 | self::assertEquals(['A', 'B'], $map->values()->toArray()); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function mapFromAssocArrayCreated(): void 40 | { 41 | $assoc = ['a' => 'A', 'b' => 'B']; 42 | $map = MapFactory::assoc($assoc); 43 | self::assertEquals(2, $map->size()); 44 | self::assertEquals(['a', 'b'], $map->keys()->toArray()); 45 | self::assertEquals(['A', 'B'], $map->values()->toArray()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/ListSequence.php: -------------------------------------------------------------------------------- 1 | 54 | */ 55 | public function stream(): Stream; 56 | } 57 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/AggregatorsFunctionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('1, 2, 3', $f(self::toCollection(1, 2, 3))); 22 | } 23 | 24 | /** 25 | * @test 26 | */ 27 | public function stringImplodingIntegration(): void 28 | { 29 | $res = self::toCollection(1, 2, 3) 30 | ->stream() 31 | ->collect(Collectors::concat(', ')); 32 | 33 | $this->assertEquals('1, 2, 3', $res); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function averageCalculating(): void 40 | { 41 | $f = Collectors::average(); 42 | $this->assertEquals(2, $f(self::toCollection(1, 2, 3))); 43 | } 44 | 45 | /** 46 | * @test 47 | */ 48 | public function averageCalculatingIntegration(): void 49 | { 50 | $res = self::toCollection(1, 2, 3) 51 | ->stream() 52 | ->collect(Collectors::average()); 53 | 54 | $this->assertEquals(2, $res); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ImmutableStreamTest.php: -------------------------------------------------------------------------------- 1 | stream(); 23 | 24 | $collection = $stream->getCollection(); 25 | 26 | $stream 27 | ->reorganize(function (Collection $c) { 28 | $c->clear(); 29 | return $this->toCollection(); 30 | }); 31 | 32 | self::assertThat($collection, CollectionIsEqual::to([1, 2, 3])); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function shouldBeImmutableInCollectWithCollection(): void 39 | { 40 | $stream = self::toCollection(1, 2, 3) 41 | ->stream(); 42 | 43 | $collection = $stream->getCollection(); 44 | $stream 45 | ->collect(static function (Collection $c) { 46 | $c->clear(); 47 | return 1; 48 | }); 49 | 50 | self::assertThat($collection, CollectionIsEqual::to([1, 2, 3])); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/ArrayQueue.php: -------------------------------------------------------------------------------- 1 | add($element); 20 | } 21 | 22 | public function poll() 23 | { 24 | if ($this->isEmpty()) { 25 | throw new RuntimeException('Queue is empty'); 26 | } 27 | 28 | $this->pointer--; 29 | return array_shift($this->elements); 30 | } 31 | 32 | public function peek() 33 | { 34 | if ($this->isEmpty()) { 35 | throw new RuntimeException('Queue is empty'); 36 | } 37 | 38 | return $this->elements[0]; 39 | } 40 | 41 | public function stream(): Stream 42 | { 43 | return new SerialStream($this); 44 | } 45 | 46 | public function getIndexIterator(): Iterator 47 | { 48 | return IteratorFactory::directSequence($this->size()); 49 | } 50 | 51 | protected function afterElementAdd($element): void 52 | { 53 | } 54 | 55 | protected function afterElementsSet(): void 56 | { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Comparators.php: -------------------------------------------------------------------------------- 1 | for scalar value compares such as int, bool, float, string 14 | * @return callable 15 | */ 16 | public static function scalarComparator(): Closure 17 | { 18 | return static function ($a, $b) { 19 | return $a <=> $b; 20 | }; 21 | } 22 | 23 | /** 24 | * Returns for scalar object property value compares such as int, bool, float, string 25 | * @param string $property 26 | * @return callable 27 | */ 28 | public static function objectPropertyComparator(string $property): Closure 29 | { 30 | return static function ($a, $b) use ($property) { 31 | return ObjectFunctions::getPropertyValue($a, $property) <=> ObjectFunctions::getPropertyValue($b, $property); 32 | }; 33 | } 34 | 35 | /** 36 | * Returns for scalar value compares such as int, bool, float, string 37 | * @param callable $f 38 | * @return Closure 39 | */ 40 | public static function callbackComparator(callable $f): Closure 41 | { 42 | return static function ($a, $b) use ($f) { 43 | return $f($a) <=> $f($b); 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Expression/BoolExpressionTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Expression; 7 | 8 | use PHPUnit\Framework\TestCase; 9 | use WS\Utils\Collections\CollectionFactory; 10 | 11 | class BoolExpressionTest extends TestCase 12 | { 13 | 14 | public function cases() 15 | { 16 | return [ 17 | [ 18 | [1, 2, 3], 19 | BoolExpression::with(function ($element) { 20 | return is_int($element); 21 | }) 22 | ->or(function ($element) { 23 | return $element === 77; 24 | }) 25 | ->and(function ($element) { 26 | return in_array($element, [1, 3]); 27 | }), 28 | [1, 3], 29 | ], 30 | ]; 31 | } 32 | 33 | /** 34 | * @test 35 | * @dataProvider cases 36 | * @param array $sequence 37 | * @param BoolExpression $expression 38 | * @param array $expected 39 | */ 40 | public function filterByExpression(array $sequence, BoolExpression $expression, array $expected) 41 | { 42 | 43 | $result = CollectionFactory::from($sequence) 44 | ->stream() 45 | ->filter($expression) 46 | ->getCollection() 47 | ->toArray(); 48 | 49 | self::assertEquals($expected, $result); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Unit tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - dev 8 | paths: 9 | - '!*.md' 10 | pull_request: 11 | branches: [ master ] 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | strategy: 17 | matrix: 18 | php-versions: ['7.1', '7.2', '7.3', '7.4'] 19 | name: Testing with PHP ${{ matrix.php-versions }} 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: ${{ matrix.php-versions }} 28 | 29 | - name: Get Composer Cache Directory 30 | id: composer-cache 31 | run: | 32 | echo "::set-output name=dir::$(composer config cache-files-dir)" 33 | 34 | - name: Cache Composer Downloads 35 | uses: actions/cache@v1.2.0 36 | with: 37 | path: ${{ steps.composer-cache.outputs.dir }} 38 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 39 | restore-keys: | 40 | ${{ runner.os }}-composer- 41 | 42 | - name: Cache PHP dependencies 43 | uses: actions/cache@v1.2.0 44 | with: 45 | path: vendor 46 | key: ${{ runner.OS }}-build-${{ hashFiles('**/composer.lock') }} 47 | 48 | - name: Validate composer.json and composer.lock 49 | run: composer validate 50 | 51 | - name: Install dependencies 52 | run: composer install --dev --no-progress 53 | 54 | - name: Run test suite 55 | run: composer run-script test 56 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/UnitConstraints/CollectionContainsSameElements.php: -------------------------------------------------------------------------------- 1 | expectedCollection = $this->normalize($expectedCollection); 20 | } 21 | 22 | private function normalize($collection) 23 | { 24 | if (is_array($collection)) { 25 | return CollectionFactory::from($collection); 26 | } 27 | 28 | return $collection; 29 | } 30 | 31 | protected function matches($other): bool 32 | { 33 | $this->normalize($other); 34 | if (!$other instanceof Collection) { 35 | throw new RuntimeException('Value of comparision need to be a Collection'); 36 | } 37 | 38 | return array_diff($other->toArray(), $this->expectedCollection->toArray()) === []; 39 | } 40 | 41 | public function toString(): string 42 | { 43 | return sprintf( 44 | 'is accepted by %s', 45 | $this->exporter()->export($this->expectedCollection) 46 | ); 47 | } 48 | 49 | public static function with($expectedValue): CollectionContainsSameElements 50 | { 51 | return new self($expectedValue); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/RemoveTraverseTrait.php: -------------------------------------------------------------------------------- 1 | isEmpty()) { 15 | return false; 16 | } 17 | $fMatch = static function ($tested) use ($element): bool { 18 | return $tested === $element; 19 | }; 20 | if ($element instanceof HashCodeAware) { 21 | $fMatch = static function ($tested) use ($element): bool { 22 | if ($tested instanceof HashCodeAware) { 23 | return $tested->getHashCode() === $element->getHashCode(); 24 | } 25 | return $tested === $element; 26 | }; 27 | } 28 | $indexIterator = $this->getIndexIterator(); 29 | $elements = $this->getElements(); 30 | while ($indexIterator->hasNext()) { 31 | $index = $indexIterator->next(); 32 | if ($fMatch($elements[$index])) { 33 | unset($elements[$index]); 34 | $this->setElements(array_values($elements)); 35 | return true; 36 | } 37 | } 38 | return false; 39 | } 40 | 41 | abstract public function getIndexIterator(): Iterator; 42 | 43 | abstract public function isEmpty(): bool; 44 | 45 | abstract protected function setElements(array $elements): void; 46 | 47 | abstract protected function getElements(): array; 48 | } 49 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Expression/BoolExpression.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Expression; 7 | 8 | use WS\Utils\Collections\Functions\Expression\Operator\AbstractOperator; 9 | use WS\Utils\Collections\Functions\Expression\Operator\AndOperator; 10 | use WS\Utils\Collections\Functions\Expression\Operator\OrOperator; 11 | 12 | class BoolExpression 13 | { 14 | 15 | /** @var AbstractOperator[] */ 16 | private $operators = []; 17 | 18 | public function __construct(callable $checker) 19 | { 20 | $this->operators[] = new AndOperator($checker); 21 | } 22 | 23 | public static function with(callable $checker): self 24 | { 25 | return new self($checker); 26 | } 27 | 28 | public function __invoke($item): bool 29 | { 30 | $operand = null; 31 | foreach ($this->operators as $operator) { 32 | if ($operand === null) { 33 | if (!$operand = $operator->getChecker()($item)) { 34 | return false; 35 | } 36 | continue; 37 | } 38 | if (!$operand = $operator($operand, $item)) { 39 | return false; 40 | } 41 | } 42 | return true; 43 | } 44 | 45 | public function and(callable $checker): self 46 | { 47 | $this->operators[] = new AndOperator($checker); 48 | return $this; 49 | } 50 | 51 | public function or(callable $checker): self 52 | { 53 | $this->operators[] = new OrOperator($checker); 54 | return $this; 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/UnitConstraints/CollectionComparingConstraint.php: -------------------------------------------------------------------------------- 1 | expectedCollection = $this->normalize($expectedCollection); 21 | } 22 | 23 | private function normalize($collection) 24 | { 25 | if (is_array($collection)) { 26 | return CollectionFactory::from($collection); 27 | } 28 | 29 | return $collection; 30 | } 31 | 32 | protected function matches($other): bool 33 | { 34 | $this->normalize($other); 35 | if (!$other instanceof Collection) { 36 | throw new RuntimeException('Value of comparision need to be a Collection'); 37 | } 38 | 39 | return $this->comparingResult($this->expectedCollection, $other); 40 | } 41 | 42 | public function toString(): string 43 | { 44 | return sprintf( 45 | 'is accepted by %s', 46 | $this->exporter->export($this->expectedCollection) 47 | ); 48 | } 49 | 50 | public static function to($expectedValue): CollectionComparingConstraint 51 | { 52 | return new static($expectedValue); 53 | } 54 | 55 | abstract public function comparingResult(Collection $expectedCollection, Collection $other): bool; 56 | } 57 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ConvertersFunctionsTest.php: -------------------------------------------------------------------------------- 1 | setName('first'), 25 | (new ExampleObject())->setName('second'), 26 | (new ExampleObject())->setName('third') 27 | ) 28 | ->stream() 29 | ->map(Converters::toPropertyValue('name')) 30 | ->getCollection() 31 | ; 32 | 33 | $this->assertThat($collection, CollectionIsEqual::to(['first', 'second', 'third'])); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function propertyValueAssocConverting(): void 40 | { 41 | $array = self::toCollection( 42 | (new ExampleObject())->setName('first')->setField('f1'), 43 | (new ExampleObject())->setName('second')->setField('f2'), 44 | (new ExampleObject())->setName('third')->setField('f3') 45 | ) 46 | ->stream() 47 | ->map(Converters::toProperties(['name', 'field'])) 48 | ->getCollection() 49 | ->toArray(); 50 | 51 | $this->assertSame($array, [ 52 | ['name' => 'first', 'field' => 'f1'], 53 | ['name' => 'second', 'field' => 'f2'], 54 | ['name' => 'third', 'field' => 'f3'] 55 | ]); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/ArrayStack.php: -------------------------------------------------------------------------------- 1 | add($element); 28 | } 29 | 30 | /** 31 | * Gets element from the top of stack 32 | * 33 | * @return mixed 34 | */ 35 | public function pop() 36 | { 37 | if ($this->isEmpty()) { 38 | throw new RuntimeException('Stack is empty'); 39 | } 40 | 41 | return array_pop($this->elements); 42 | } 43 | 44 | /** 45 | * Retrieves, but does not remove 46 | * 47 | * @return mixed 48 | */ 49 | public function peek() 50 | { 51 | if ($this->isEmpty()) { 52 | throw new RuntimeException('Stack is empty'); 53 | } 54 | 55 | return $this->elements[count($this->elements) - 1]; 56 | } 57 | 58 | public function stream(): Stream 59 | { 60 | return new SerialStream($this); 61 | } 62 | 63 | public function toArray(): array 64 | { 65 | return array_reverse($this->elements); 66 | } 67 | 68 | public function getIndexIterator(): Iterator 69 | { 70 | return IteratorFactory::reverseSequence($this->size()); 71 | } 72 | 73 | protected function afterElementAdd($element): void 74 | { 75 | } 76 | 77 | protected function afterElementsSet(): void 78 | { 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Collection.php: -------------------------------------------------------------------------------- 1 | compare($list, function ($list) { 32 | /** @var Collection $shuffledCollection */ 33 | $shuffledCollection = Reorganizers::shuffle()($this->toCollection($list)); 34 | 35 | return $shuffledCollection->toArray(); 36 | }); 37 | } 38 | 39 | /** 40 | * @dataProvider cases 41 | * @test 42 | */ 43 | public function integration($list) 44 | { 45 | 46 | $this->compare($list, function ($list) { 47 | return $this->toCollection($list) 48 | ->stream() 49 | ->reorganize(Reorganizers::shuffle()) 50 | ->getCollection() 51 | ->toArray(); 52 | }); 53 | } 54 | 55 | private function compare($list, callable $shuffle): void 56 | { 57 | $shuffled = []; 58 | for ($i = 0; $i < 10; $i++) { 59 | $shuffled = $shuffle($list); 60 | if ($shuffled !== $list) { 61 | break; 62 | } 63 | if (count($list) === 1 && count($shuffled) === 1) { 64 | $this->assertTrue(true); 65 | return; 66 | } 67 | } 68 | 69 | $this->assertNotEquals($list, $shuffled); 70 | $this->assertCount(count($list), $shuffled); 71 | 72 | foreach ($list as $item) { 73 | $found = array_search($item, $shuffled, true); 74 | if ($found === false) { 75 | $this->fail("Item {$item} is not found in shuffled array"); 76 | } 77 | 78 | $shuffled = array_diff($shuffled, [$item]); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/RandomReorganizerTest.php: -------------------------------------------------------------------------------- 1 | toCollection($elements))->toArray(); 38 | 39 | $this->analyze($elements, $randomized, $count); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function failedReorganize(): void 46 | { 47 | $this->expectException(RuntimeException::class); 48 | $this->toCollection([1, 2, 3]) 49 | ->stream() 50 | ->reorganize(static function () { 51 | return [1, 2, 3]; 52 | }) 53 | ->getCollection() 54 | ->toArray() 55 | ; 56 | } 57 | 58 | /** 59 | * @dataProvider cases 60 | * @test 61 | * @param array $elements 62 | * @param int $count 63 | */ 64 | public function integrate(array $elements, int $count): void 65 | { 66 | $randomized = $this->toCollection($elements) 67 | ->stream() 68 | ->reorganize(Reorganizers::random($count)) 69 | ->getCollection() 70 | ->toArray() 71 | ; 72 | 73 | $this->analyze($elements, $randomized, $count); 74 | } 75 | 76 | private function analyze(array $input, array $randomized, int $count): void 77 | { 78 | $this->assertTrue(count($input) >= count($randomized)); 79 | $this->assertTrue(count($randomized) <= $count); 80 | if (count($input) >= $count) { 81 | $this->assertCount($count, $randomized); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/CollectionFactory.php: -------------------------------------------------------------------------------- 1 | for $times 16 | * @param int $times 17 | * @param callable|null $generator 18 | * @return Collection 19 | */ 20 | public static function generate(int $times, ?callable $generator = null): Collection 21 | { 22 | if ($times < 0) { 23 | throw new RuntimeException('The count of values ($times) must be a positive value'); 24 | } 25 | $generator = $generator ?? static function (int $index) { 26 | return $index; 27 | }; 28 | 29 | $collection = self::toCollection(); 30 | for ($i = 0; $i < $times; $i++) { 31 | $collection->add($generator($i)); 32 | } 33 | 34 | return $collection; 35 | } 36 | 37 | /** 38 | * Generate collection of int numbers between $from and $to. If $to arg is absent $from - is count of numbers 39 | * @param int $from 40 | * @param int $to 41 | * @return Collection 42 | */ 43 | public static function numbers(int $from, ?int $to = null): Collection 44 | { 45 | if ($to === null) { 46 | $to = $from - 1; 47 | $from = 0; 48 | } 49 | 50 | if ($from > $to) { 51 | throw new RuntimeException('FROM need to be less than TO'); 52 | } 53 | $list = new ArrayList(); 54 | for ($i = $from; $i <= $to; $i++) { 55 | $list->add($i); 56 | } 57 | 58 | return $list; 59 | } 60 | 61 | public static function from(array $values): Collection 62 | { 63 | return new ArrayList($values); 64 | } 65 | 66 | public static function fromStrict(array $values): Collection 67 | { 68 | return new ArrayStrictList($values); 69 | } 70 | 71 | public static function fromIterable(iterable $iterable): Collection 72 | { 73 | $list = ArrayList::of(); 74 | foreach ($iterable as $item) { 75 | $list->add($item); 76 | } 77 | 78 | return $list; 79 | } 80 | 81 | public static function empty(): Collection 82 | { 83 | return ArrayList::of(); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/CollectionFactoryTest.php: -------------------------------------------------------------------------------- 1 | size(); 23 | $this->assertEquals(10, $sizeOfTen); 24 | } 25 | 26 | /** 27 | * @test 28 | */ 29 | public function failSequenceGenerating(): void 30 | { 31 | $this->expectException(RuntimeException::class); 32 | CollectionFactory::generate(-10); 33 | } 34 | 35 | /** 36 | * @test 37 | */ 38 | public function sequenceWithGeneratorGenerating(): void 39 | { 40 | $i = 0; 41 | $sizeOfTen = CollectionFactory::generate(10, static function () use (& $i) { 42 | return $i++; 43 | }) 44 | ->size(); 45 | $this->assertEquals(10, $sizeOfTen); 46 | } 47 | 48 | /** 49 | * @test 50 | */ 51 | public function numbersFactoryTest(): void 52 | { 53 | $sizeOfTwenty = CollectionFactory::numbers(-10, 10)->size(); 54 | $this->assertEquals(21, $sizeOfTwenty); 55 | 56 | $sizeOfTen = CollectionFactory::numbers(10)->size(); 57 | $this->assertEquals(10, $sizeOfTen); 58 | 59 | $this->expectException(RuntimeException::class); 60 | CollectionFactory::numbers(-10); 61 | } 62 | 63 | /** 64 | * @test 65 | */ 66 | public function creatingFromArray(): void 67 | { 68 | $collection = CollectionFactory::from([1, 2, 3]); 69 | 70 | $this->assertEquals(3, $collection->size()); 71 | $this->assertThat($collection, CollectionIsEqual::to([1, 2, 3])); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function creatingFromStrictArray(): void 78 | { 79 | $collection = CollectionFactory::fromStrict([4, 5, 6]); 80 | 81 | $this->assertEquals(3, $collection->size()); 82 | $this->assertThat($collection, CollectionIsEqual::to([4, 5, 6])); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function fromIteratorCollectionGenerating(): void 89 | { 90 | $collection = CollectionFactory::fromIterable(new ArrayIterator([1, 2, 3])); 91 | $this->assertThat($collection, CollectionIsEqual::to([1, 2, 3])); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/CountTest.php: -------------------------------------------------------------------------------- 1 | 'asdf'], 19 | [], 20 | [], 21 | ['one' => 999], 22 | ['one' => false], 23 | ['one' => true], 24 | ], 25 | 6 26 | ], 27 | [ 28 | [ 29 | ['count' => 1], 30 | ['count' => 2], 31 | ['count' => 3], 32 | ], 33 | 3 34 | ], 35 | [ 36 | [ 37 | new class () { 38 | public $test = 1; 39 | }, 40 | new class () { 41 | public $test = 1; 42 | }, 43 | new class () { 44 | public $test = 1; 45 | }, 46 | new class () { 47 | public $test = 15; 48 | }, 49 | ], 50 | 4 51 | ], 52 | [ 53 | [ 54 | new class () { 55 | public function getSum() 56 | { 57 | return 10; 58 | } 59 | }, 60 | new class () { 61 | public function getSum() 62 | { 63 | return 10; 64 | } 65 | }, 66 | new class () { 67 | public function getSum() 68 | { 69 | return 20; 70 | } 71 | }, 72 | ], 73 | 3 74 | ], 75 | ]; 76 | } 77 | 78 | /** 79 | * @dataProvider cases 80 | * @test 81 | * @param $collection 82 | * @param $expected 83 | */ 84 | public function callSuccess($collection, $expected) 85 | { 86 | $aggregator = new Count(); 87 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ImmutableListTest.php: -------------------------------------------------------------------------------- 1 | createInstance(); 47 | 48 | $this->expectException(RuntimeException::class); 49 | call_user_func_array([$list, $method], $args); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function gettingListInfo(): void 56 | { 57 | $list = $this->createInstance(1, 2, 3); 58 | 59 | $this->assertEquals(2, $list->get(1)); 60 | $this->assertTrue($list->contains(3)); 61 | $this->assertEquals(3, $list->size()); 62 | $this->assertEquals(2, $list->indexOf(3)); 63 | $this->assertEquals(0, $list->lastIndexOf(1)); 64 | $this->assertFalse($list->isEmpty()); 65 | $this->assertTrue($list->equals(ArrayList::of(1, 2, 3))); 66 | $this->assertEquals([1, 2, 3], $list->toArray()); 67 | $this->assertThat($list->copy(), CollectionIsEqual::to($list)); 68 | /** @noinspection UnnecessaryAssertionInspection */ 69 | $this->assertInstanceOf(Stream::class, $list->stream()); 70 | /** @noinspection PhpUnhandledExceptionInspection */ 71 | $this->assertInstanceOf(Traversable::class, $list->getIterator()); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function creating(): void 78 | { 79 | $instance = ImmutableList::fromCollection($this->createInstance(1, 2)); 80 | $this->assertEquals(2, $instance->size()); 81 | 82 | $instance = ImmutableList::of(1, 2); 83 | $this->assertEquals(2, $instance->size()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/HashSet.php: -------------------------------------------------------------------------------- 1 | internalMap = new HashMap(); 21 | if ($elements !== null) { 22 | foreach ($elements as $element) { 23 | $this->add($element); 24 | } 25 | } 26 | } 27 | 28 | public function add($element): bool 29 | { 30 | return $this->internalMap->put($element, null); 31 | } 32 | 33 | public function stream(): Stream 34 | { 35 | return new SerialStream($this); 36 | } 37 | 38 | public function merge(Collection $collection): bool 39 | { 40 | foreach ($collection as $item) { 41 | $this->add($item); 42 | } 43 | return true; 44 | } 45 | 46 | public function clear(): void 47 | { 48 | $this->internalMap = new HashMap(); 49 | } 50 | 51 | public function remove($element): bool 52 | { 53 | return $this->internalMap->remove($element); 54 | } 55 | 56 | public function contains($element): bool 57 | { 58 | return $this->internalMap->containsKey($element); 59 | } 60 | 61 | public function equals(Collection $collection): bool 62 | { 63 | if ($this->size() !== $collection->size()) { 64 | return false; 65 | } 66 | foreach ($collection as $item) { 67 | if (!$this->contains($item)) { 68 | return false; 69 | } 70 | } 71 | return true; 72 | } 73 | 74 | public function size(): int 75 | { 76 | return $this->internalMap->size(); 77 | } 78 | 79 | public function isEmpty(): bool 80 | { 81 | return $this->size() === 0; 82 | } 83 | 84 | public function toArray(): array 85 | { 86 | return $this->internalMap->keys()->toArray(); 87 | } 88 | 89 | public function copy(): Collection 90 | { 91 | return new static($this->toArray()); 92 | } 93 | 94 | public function getIterator() 95 | { 96 | return new ArrayIterator($this->toArray()); 97 | } 98 | 99 | public function addAll(iterable $elements): bool 100 | { 101 | $res = true; 102 | foreach ($elements as $element) { 103 | !$this->add($element) && $res = false; 104 | } 105 | return $res; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/FirstTest.php: -------------------------------------------------------------------------------- 1 | 'asdf'], 19 | [], 20 | [], 21 | ['one' => 999], 22 | ['one' => false], 23 | ['one' => true], 24 | ], 25 | 'asdf', 26 | ], 27 | [ 28 | 'count', 29 | [ 30 | [], 31 | ['count' => 2], 32 | ['count' => 3], 33 | ], 34 | null 35 | ], 36 | [ 37 | 'test', 38 | [ 39 | new class () { 40 | public $test = 1; 41 | }, 42 | new class () { 43 | public $test = 1; 44 | }, 45 | new class () { 46 | public $test = 1; 47 | }, 48 | new class () { 49 | public $test = 15; 50 | }, 51 | ], 52 | 1 53 | ], 54 | [ 55 | 'sum', 56 | [ 57 | new class () { 58 | public function getSum() 59 | { 60 | return 10; 61 | } 62 | }, 63 | new class () { 64 | public function getSum() 65 | { 66 | return 10; 67 | } 68 | }, 69 | new class () { 70 | public function getSum() 71 | { 72 | return 20; 73 | } 74 | }, 75 | ], 76 | 10 77 | ], 78 | ]; 79 | } 80 | 81 | /** 82 | * @dataProvider cases 83 | * @test 84 | * @param $column 85 | * @param $collection 86 | * @param $expected 87 | */ 88 | public function callSuccess($column, $collection, $expected) 89 | { 90 | $aggregator = new First($column); 91 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/AddToSetTest.php: -------------------------------------------------------------------------------- 1 | 'asdf'], 20 | [], 21 | [], 22 | ['one' => 999], 23 | ['one' => false], 24 | ['one' => true], 25 | ], 26 | ['asdf', null, 999, false, true] 27 | ], 28 | [ 29 | 'count', 30 | [ 31 | ['count' => 1], 32 | ['count' => 2], 33 | ['count' => 3], 34 | ], 35 | [1, 2, 3] 36 | ], 37 | [ 38 | 'test', 39 | [ 40 | new class () { 41 | public $test = 1; 42 | }, 43 | new class () { 44 | public $test = 1; 45 | }, 46 | new class () { 47 | public $test = 1; 48 | }, 49 | new class () { 50 | public $test = 15; 51 | }, 52 | ], 53 | [1, 15] 54 | ], 55 | [ 56 | 'sum', 57 | [ 58 | new class () { 59 | public function getSum() 60 | { 61 | return 10; 62 | } 63 | }, 64 | new class () { 65 | public function getSum() 66 | { 67 | return 10; 68 | } 69 | }, 70 | new class () { 71 | public function getSum() 72 | { 73 | return 20; 74 | } 75 | }, 76 | ], 77 | [10, 20] 78 | ], 79 | ]; 80 | } 81 | 82 | /** 83 | * @dataProvider cases 84 | * @test 85 | * @param $column 86 | * @param $collection 87 | * @param $expected 88 | */ 89 | public function callSuccess($column, $collection, $expected) 90 | { 91 | $aggregator = new AddToSet($column); 92 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/LastTest.php: -------------------------------------------------------------------------------- 1 | 'asdf'], 25 | [], 26 | [], 27 | ['one' => 999], 28 | ['one' => false], 29 | ['one' => true], 30 | ], 31 | true, 32 | ], 33 | [ 34 | 'count', 35 | [ 36 | [], 37 | ['count' => 2], 38 | ['count' => 3], 39 | ], 40 | 3 41 | ], 42 | [ 43 | 'test', 44 | [ 45 | new class () { 46 | public $test = 1; 47 | }, 48 | new class () { 49 | public $test = 1; 50 | }, 51 | new class () { 52 | public $test = 1; 53 | }, 54 | new class () { 55 | public $test = 15; 56 | }, 57 | ], 58 | 15 59 | ], 60 | [ 61 | 'sum', 62 | [ 63 | new class () { 64 | public function getSum() 65 | { 66 | return 10; 67 | } 68 | }, 69 | new class () { 70 | public function getSum() 71 | { 72 | return 10; 73 | } 74 | }, 75 | new class () { 76 | public function getSum() 77 | { 78 | return 20; 79 | } 80 | }, 81 | ], 82 | 20 83 | ], 84 | ]; 85 | } 86 | 87 | /** 88 | * @dataProvider cases 89 | * @test 90 | * @param $column 91 | * @param $collection 92 | * @param $expected 93 | */ 94 | public function callSuccess($column, $collection, $expected) 95 | { 96 | $aggregator = new Last($column); 97 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/ArrayList.php: -------------------------------------------------------------------------------- 1 | elements[$index]; 21 | } 22 | 23 | public function set($element, int $index) 24 | { 25 | if (!isset($this->elements[$index])) { 26 | throw new OutOfRangeException("Index $index is out of list range with size: {$this->size()} "); 27 | } 28 | $res = $this->elements[$index]; 29 | $this->elements[$index] = $element; 30 | $this->afterElementAdd($element); 31 | 32 | return $res; 33 | } 34 | 35 | public function indexOf($element): ?int 36 | { 37 | return array_search($element, $this->elements, true) ?: null; 38 | } 39 | 40 | public function remove($element): bool 41 | { 42 | if (is_object($element) && $element instanceof HashCodeAware) { 43 | return $this->removeThroughHashCode($element); 44 | } 45 | $key = array_search($element, $this->elements, true); 46 | if (false === $key) { 47 | return false; 48 | } 49 | $this->removeAt($key); 50 | return true; 51 | } 52 | 53 | public function lastIndexOf($element): ?int 54 | { 55 | $reverseIndex = array_search($element, array_reverse($this->elements), true); 56 | if ($reverseIndex === false) { 57 | return null; 58 | } 59 | 60 | return count($this->elements) - $reverseIndex - 1; 61 | } 62 | 63 | public function removeAt(int $index) 64 | { 65 | $size = $this->size(); 66 | if ($index >= $size) { 67 | return null; 68 | } 69 | 70 | $el = $this->elements[$index]; 71 | unset($this->elements[$index]); 72 | $this->pointer--; 73 | if ($this->pointer === -1) { 74 | return $el; 75 | } 76 | $this->elements = array_merge( 77 | array_slice($this->elements, 0, $index), 78 | array_slice($this->elements, $index) 79 | ); 80 | return $el; 81 | } 82 | 83 | private function removeThroughHashCode(HashCodeAware $element): bool 84 | { 85 | foreach ($this->elements as $i => $iElement) { 86 | if ($iElement instanceof HashCodeAware && $iElement->getHashCode() === $element->getHashCode()) { 87 | $this->removeAt($i); 88 | return true; 89 | } 90 | } 91 | return false; 92 | } 93 | 94 | protected function afterElementAdd($element): void 95 | { 96 | } 97 | 98 | protected function afterElementsSet(): void 99 | { 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/MaxTest.php: -------------------------------------------------------------------------------- 1 | 0], 29 | [], 30 | ['price' => 23], 31 | [], 32 | ['price' => 10], 33 | ], 34 | 23 35 | ], 36 | [ 37 | 'count', 38 | [ 39 | ['count' => 1], 40 | ['count' => 2], 41 | ['count' => 3], 42 | ['count' => 4], 43 | ['count' => 5], 44 | ], 45 | 5 46 | ], 47 | [ 48 | 'test', 49 | [ 50 | new class () { 51 | public $test = 1; 52 | }, 53 | new class () { 54 | public $test = 2; 55 | }, 56 | new class () { 57 | public $test = 3; 58 | }, 59 | new class () { 60 | public $test = 4; 61 | }, 62 | new class () { 63 | public $test = 15; 64 | }, 65 | ], 66 | 15 67 | ], 68 | [ 69 | 'sum', 70 | [ 71 | new class () { 72 | public function getSum() 73 | { 74 | return 10; 75 | } 76 | }, 77 | new class () { 78 | public function getSum() 79 | { 80 | return 20; 81 | } 82 | }, 83 | ], 84 | 20 85 | ], 86 | ]; 87 | } 88 | 89 | /** 90 | * @dataProvider cases 91 | * @test 92 | * @param $column 93 | * @param $collection 94 | * @param $expected 95 | */ 96 | public function callSuccess($column, $collection, $expected) 97 | { 98 | $aggregator = new Max($column); 99 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/MinTest.php: -------------------------------------------------------------------------------- 1 | 0], 29 | [], 30 | ['price' => 23], 31 | [], 32 | ['price' => 10], 33 | ], 34 | 0 35 | ], 36 | [ 37 | 'count', 38 | [ 39 | ['count' => 1], 40 | ['count' => 2], 41 | ['count' => 3], 42 | ['count' => 4], 43 | ['count' => 5], 44 | ], 45 | 1 46 | ], 47 | [ 48 | 'test', 49 | [ 50 | new class () { 51 | public $test = 1; 52 | }, 53 | new class () { 54 | public $test = 2; 55 | }, 56 | new class () { 57 | public $test = -3; 58 | }, 59 | new class () { 60 | public $test = 4; 61 | }, 62 | new class () { 63 | public $test = 15; 64 | }, 65 | ], 66 | -3 67 | ], 68 | [ 69 | 'sum', 70 | [ 71 | new class () { 72 | public function getSum() 73 | { 74 | return 10; 75 | } 76 | }, 77 | new class () { 78 | public function getSum() 79 | { 80 | return 20; 81 | } 82 | }, 83 | ], 84 | 10 85 | ], 86 | ]; 87 | } 88 | 89 | /** 90 | * @dataProvider cases 91 | * @test 92 | * @param $column 93 | * @param $collection 94 | * @param $expected 95 | */ 96 | public function callSuccess($column, $collection, $expected) 97 | { 98 | $aggregator = new Min($column); 99 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/SumTest.php: -------------------------------------------------------------------------------- 1 | 0], 29 | [], 30 | ['price' => 23], 31 | [], 32 | ['price' => 10], 33 | ], 34 | 33 35 | ], 36 | [ 37 | 'count', 38 | [ 39 | ['count' => 1], 40 | ['count' => 2], 41 | ['count' => 3], 42 | ['count' => 4], 43 | ['count' => 5], 44 | ], 45 | 15 46 | ], 47 | [ 48 | 'test', 49 | [ 50 | new class () { 51 | public $test = 1; 52 | }, 53 | new class () { 54 | public $test = 2; 55 | }, 56 | new class () { 57 | public $test = -3; 58 | }, 59 | new class () { 60 | public $test = 4; 61 | }, 62 | new class () { 63 | public $test = 15; 64 | }, 65 | ], 66 | 19 67 | ], 68 | [ 69 | 'sum', 70 | [ 71 | new class () { 72 | public function getSum() 73 | { 74 | return 10; 75 | } 76 | }, 77 | new class () { 78 | public function getSum() 79 | { 80 | return 20; 81 | } 82 | }, 83 | ], 84 | 30 85 | ], 86 | ]; 87 | } 88 | 89 | /** 90 | * @dataProvider cases 91 | * @test 92 | * @param $column 93 | * @param $collection 94 | * @param $expected 95 | */ 96 | public function callSuccess($column, $collection, $expected) 97 | { 98 | $aggregator = new Sum($column); 99 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 100 | } 101 | 102 | } 103 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/AbstractCollection.php: -------------------------------------------------------------------------------- 1 | setElements($elements); 19 | } 20 | 21 | public static function of(...$elements): self 22 | { 23 | return new static($elements ?: null); 24 | } 25 | 26 | public function add($element): bool 27 | { 28 | if ($this->pointer === PHP_INT_MAX) { 29 | return false; 30 | } 31 | $this->pointer++; 32 | $this->elements[] = $element; 33 | $this->afterElementAdd($element); 34 | return true; 35 | } 36 | 37 | public function addAll(iterable $elements): bool 38 | { 39 | foreach ($elements as $element) { 40 | $this->elements[] = $element; 41 | $this->afterElementAdd($element); 42 | } 43 | $newPointer = count($this->elements) - 1; 44 | if ($newPointer > PHP_INT_MAX) { 45 | $this->elements = array_slice($this->elements, 0, $this->pointer); 46 | } else { 47 | $this->pointer = $newPointer; 48 | } 49 | return true; 50 | } 51 | 52 | public function merge(Collection $collection): bool 53 | { 54 | return $this->addAll($collection->toArray()); 55 | } 56 | 57 | public function clear(): void 58 | { 59 | $this->setElements([]); 60 | } 61 | 62 | public function contains($element): bool 63 | { 64 | return in_array($element, $this->elements, true); 65 | } 66 | 67 | public function equals(Collection $collection): bool 68 | { 69 | return $this->toArray() === $collection->toArray(); 70 | } 71 | 72 | public function size(): int 73 | { 74 | return $this->pointer + 1; 75 | } 76 | 77 | public function isEmpty(): bool 78 | { 79 | return $this->pointer === -1; 80 | } 81 | 82 | public function toArray(): array 83 | { 84 | return $this->elements; 85 | } 86 | 87 | public function getIterator() 88 | { 89 | yield from $this->toArray(); 90 | } 91 | 92 | public function copy(): Collection 93 | { 94 | return clone $this; 95 | } 96 | 97 | protected function setElements(array $elements): void 98 | { 99 | $this->elements = array_values($elements); 100 | $this->pointer = count($elements) - 1; 101 | $this->afterElementsSet(); 102 | } 103 | 104 | protected function getElements(): array 105 | { 106 | return $this->elements; 107 | } 108 | 109 | abstract protected function afterElementAdd($element): void; 110 | 111 | abstract protected function afterElementsSet(): void; 112 | 113 | abstract public function stream(): Stream; 114 | } 115 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ArrayStrictListTest.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections; 7 | 8 | use InvalidArgumentException; 9 | use PHPUnit\Framework\TestCase; 10 | 11 | class ArrayStrictListTest extends TestCase 12 | { 13 | public function createInstance(...$args): ListSequence 14 | { 15 | return ArrayStrictList::of(...$args); 16 | } 17 | 18 | public function strictCases(): array 19 | { 20 | return [ 21 | [1, 2, 3], 22 | [1.1, 2.2, 3.3], 23 | ['one', 'two', 'three'], 24 | [true, false, true], 25 | [[1], ['2'], [3.3]], 26 | [null, null, null], 27 | [ 28 | function () { 29 | return '1'; 30 | }, 31 | function () { 32 | return 'two'; 33 | }, 34 | function () { 35 | return 3; 36 | }, 37 | ], 38 | [ 39 | $object = new class () {}, 40 | clone $object, 41 | clone $object, 42 | ], 43 | ]; 44 | } 45 | 46 | public function notStrictCases(): array 47 | { 48 | return [ 49 | [1, '2', 3], 50 | [1.1, 2.2, 3], 51 | ['one', 'two', 3.3], 52 | ['true', false, true], 53 | [[1], null, [3.3]], 54 | [null, null, []], 55 | [ 56 | function () { 57 | return '1'; 58 | }, 59 | new class () {}, 60 | function () { 61 | return 3; 62 | }, 63 | ], 64 | [ 65 | $object = new class () {}, 66 | clone $object, 67 | new class () {}, 68 | ], 69 | ]; 70 | } 71 | 72 | /** 73 | * @test 74 | * @dataProvider strictCases 75 | * @doesNotPerformAssertions 76 | * @param $sequence 77 | */ 78 | public function creatingFromStrict(...$sequence): void 79 | { 80 | $this->createInstance(...$sequence); 81 | } 82 | 83 | /** 84 | * @test 85 | * @dataProvider notStrictCases 86 | * @param $sequence 87 | */ 88 | public function creatingFromNotStrict(...$sequence): void 89 | { 90 | self::expectException(InvalidArgumentException::class); 91 | $this->createInstance(...$sequence); 92 | } 93 | 94 | /** 95 | * @test 96 | */ 97 | public function addingElementsToStrictArray() 98 | { 99 | self::expectException(InvalidArgumentException::class); 100 | 101 | $this 102 | ->createInstance(1, 2, 3) 103 | ->add('4'); 104 | 105 | $this 106 | ->createInstance('one', 'two', 'three') 107 | ->addAll(['4', 5, '6']); 108 | 109 | $object = new class () {}; 110 | $this 111 | ->createInstance($object, clone $object, clone $object) 112 | ->set(new class () {}, 2); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/Functions/Group/Aggregator/AvgTest.php: -------------------------------------------------------------------------------- 1 | 25], 27 | [], 28 | [], 29 | [], 30 | ['count' => 25], 31 | ], 32 | 10 33 | ], 34 | [ 35 | 'count', 36 | [ 37 | ['count' => 1], 38 | ['count' => 2], 39 | ['count' => 3], 40 | ['count' => 4], 41 | ['count' => 5], 42 | ], 43 | 3 44 | ], 45 | [ 46 | 'test', 47 | [ 48 | new class () { 49 | public $test = 1; 50 | }, 51 | new class () { 52 | public $test = 2; 53 | }, 54 | new class () { 55 | public $test = 3; 56 | }, 57 | new class () { 58 | public $test = 4; 59 | }, 60 | new class () { 61 | public $test = 15; 62 | }, 63 | ], 64 | 5 65 | ], 66 | [ 67 | 'sum', 68 | [ 69 | new class () { 70 | public function getSum() 71 | { 72 | return 10; 73 | } 74 | }, 75 | new class () { 76 | public function getSum() 77 | { 78 | return 20; 79 | } 80 | }, 81 | ], 82 | 15 83 | ], 84 | ]; 85 | } 86 | 87 | /** 88 | * @dataProvider cases 89 | * @test 90 | * @param $column 91 | * @param $collection 92 | * @param $expected 93 | */ 94 | public function callSuccess($column, $collection, $expected) 95 | { 96 | $aggregator = new Avg($column); 97 | self::assertEquals($expected, $aggregator($this->toCollection($collection))); 98 | } 99 | 100 | /** 101 | * @test 102 | */ 103 | public function raisedException() 104 | { 105 | $this->expectException(RuntimeException::class); 106 | $aggregator = new Avg('test'); 107 | $aggregator($this->toCollection([new stdClass(), new stdClass(), new stdClass()])); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/ImmutableList.php: -------------------------------------------------------------------------------- 1 | decoratedList = new ArrayList($elements); 18 | } 19 | 20 | public static function fromCollection(Collection $collection): self 21 | { 22 | return new static($collection->toArray()); 23 | } 24 | 25 | public static function of(...$elements): self 26 | { 27 | return new static($elements ?: null); 28 | } 29 | 30 | public function stream(): Stream 31 | { 32 | return $this->decoratedList->stream(); 33 | } 34 | 35 | public function remove($element): bool 36 | { 37 | throw $this->createBlockingException(); 38 | } 39 | 40 | public function get(int $index) 41 | { 42 | return $this->decoratedList->get($index); 43 | } 44 | 45 | public function set($element, int $index) 46 | { 47 | throw $this->createBlockingException(); 48 | } 49 | 50 | public function indexOf($element): ?int 51 | { 52 | return $this->decoratedList->indexOf($element); 53 | } 54 | 55 | public function lastIndexOf($element): ?int 56 | { 57 | return $this->decoratedList->lastIndexOf($element); 58 | } 59 | 60 | public function removeAt(int $index) 61 | { 62 | throw $this->createBlockingException(); 63 | } 64 | 65 | private function createBlockingException(): RuntimeException 66 | { 67 | return new RuntimeException('Is immutable list. Everything modifier call is prohibited'); 68 | } 69 | 70 | public function add($element): bool 71 | { 72 | throw $this->createBlockingException(); 73 | } 74 | 75 | public function addAll(iterable $elements): bool 76 | { 77 | throw $this->createBlockingException(); 78 | } 79 | 80 | public function merge(Collection $collection): bool 81 | { 82 | throw $this->createBlockingException(); 83 | } 84 | 85 | public function clear(): void 86 | { 87 | throw $this->createBlockingException(); 88 | } 89 | 90 | public function contains($element): bool 91 | { 92 | return $this->decoratedList->contains($element); 93 | } 94 | 95 | public function equals(Collection $collection): bool 96 | { 97 | return $this->decoratedList->equals($collection); 98 | } 99 | 100 | public function size(): int 101 | { 102 | return $this->decoratedList->size(); 103 | } 104 | 105 | public function isEmpty(): bool 106 | { 107 | return $this->decoratedList->isEmpty(); 108 | } 109 | 110 | public function toArray(): array 111 | { 112 | return $this->decoratedList->toArray(); 113 | } 114 | 115 | public function copy(): Collection 116 | { 117 | return $this->decoratedList->copy(); 118 | } 119 | 120 | public function getIterator() 121 | { 122 | return $this->decoratedList->getIterator(); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ArrayQueueTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($queueProvider, $queue->toArray()); 27 | } 28 | 29 | public function testOffer() 30 | { 31 | $queueProvider = [100, 0, 20, 10, 9]; 32 | $queue = ArrayQueue::of(...$queueProvider); 33 | $this->assertTrue($queue->offer(100)); 34 | $this->assertTrue($queue->offer(100)); 35 | $this->assertTrue($queue->offer(101)); 36 | $this->assertEquals(count($queueProvider) + 3, $queue->size()); 37 | $this->assertEquals(array_merge($queueProvider, [100, 100, 101]), $queue->toArray()); 38 | } 39 | 40 | public function testPoll() 41 | { 42 | $queueProvider = [100, 0, 20, 10, 9]; 43 | $queue = ArrayQueue::of(...$queueProvider); 44 | 45 | $this->assertEquals(100, $queue->poll()); 46 | $this->assertEquals(count($queueProvider) - 1, $queue->size()); 47 | $this->assertEquals(0, $queue->poll()); 48 | $this->assertEquals(20, $queue->poll()); 49 | $this->assertEquals(count($queueProvider) - 3, $queue->size()); 50 | } 51 | 52 | public function testPollEmpty() 53 | { 54 | $this->expectException(RuntimeException::class); 55 | $queueProvider = []; 56 | $queue = ArrayQueue::of(...$queueProvider); 57 | $queue->poll(); 58 | } 59 | 60 | public function testPeek() 61 | { 62 | $queueProvider = [100, 0, 20, 10, 9]; 63 | $queue = ArrayQueue::of(...$queueProvider); 64 | 65 | $this->assertEquals(100, $queue->peek()); 66 | $this->assertEquals(count($queueProvider), $queue->size()); 67 | $this->assertEquals(100, $queue->peek()); 68 | $this->assertEquals(100, $queue->peek()); 69 | $this->assertEquals(count($queueProvider), $queue->size()); 70 | $queue->poll(); 71 | $queue->poll(); 72 | $this->assertEquals(20, $queue->peek()); 73 | } 74 | 75 | public function testPeekEmpty() 76 | { 77 | $this->expectException(RuntimeException::class); 78 | $queueProvider = []; 79 | $queue = ArrayQueue::of(...$queueProvider); 80 | $queue->peek(); 81 | } 82 | 83 | public function testAll() { 84 | $queue = ArrayQueue::of(); 85 | $queue->add(2); 86 | $queue->add(3); 87 | $queue->add(1); 88 | 89 | $this->assertEquals(3, $queue->size()); 90 | 91 | $this->assertEquals(2, $queue->peek()); 92 | $this->assertEquals(2, $queue->poll()); 93 | $queue->add(10); 94 | $this->assertFalse($queue->isEmpty()); 95 | $this->assertEquals(3, $queue->peek()); 96 | $this->assertEquals(3, $queue->poll()); 97 | $this->assertEquals(1, $queue->poll()); 98 | $this->assertEquals(10, $queue->poll()); 99 | $this->assertEquals(0, $queue->size()); 100 | $this->assertTrue($queue->isEmpty()); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/AggregateGroupingTest.php: -------------------------------------------------------------------------------- 1 | stream() 26 | ->collect(Collectors::group()) 27 | ; 28 | 29 | $this->assertEquals(1, $map->get(1)); 30 | $this->assertEquals(3, $map->get(2)); 31 | $this->assertEquals(2, $map->get(3)); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function groupingByObjects(): void 38 | { 39 | $o1 = new SplObjectStorage(); 40 | $o2 = new SplObjectStorage(); 41 | $o3 = new SplObjectStorage(); 42 | 43 | $map = self::toCollection($o1, $o2, $o3, $o3, $o2, $o2) 44 | ->stream() 45 | ->collect(Collectors::group()) 46 | ; 47 | 48 | $this->assertEquals(1, $map->get($o1)); 49 | $this->assertEquals(3, $map->get($o2)); 50 | $this->assertEquals(2, $map->get($o3)); 51 | } 52 | 53 | /** 54 | * @test 55 | */ 56 | public function groupingByObjectsWithHashCode(): void 57 | { 58 | $map = self::toCollection( 59 | new TestInteger(1), 60 | new TestInteger(2), 61 | new TestInteger(3), 62 | new TestInteger(3), 63 | new TestInteger(2), 64 | new TestInteger(2) 65 | ) 66 | ->stream() 67 | ->collect(Collectors::group()) 68 | ; 69 | 70 | $this->assertEquals(1, $map->get(new TestInteger(1))); 71 | $this->assertEquals(3, $map->get(new TestInteger(2))); 72 | $this->assertEquals(2, $map->get(new TestInteger(3))); 73 | } 74 | 75 | /** 76 | * @test 77 | */ 78 | public function groupingByFunction(): void 79 | { 80 | $map = self::toCollection( 81 | 1, 2, 3, 3, 2, 2 82 | ) 83 | ->stream() 84 | ->collect(Collectors::groupBy(static function ($v) { 85 | return $v * 10; 86 | })) 87 | ; 88 | 89 | $this->assertEquals(1, $map->get(10)); 90 | $this->assertEquals(3, $map->get(20)); 91 | $this->assertEquals(2, $map->get(30)); 92 | } 93 | 94 | /** 95 | * @test 96 | */ 97 | public function groupingByObjectProperty(): void 98 | { 99 | $o1 = (new ExampleObject())->setName('first'); 100 | $o2 = (new ExampleObject())->setName('second'); 101 | $o3 = (new ExampleObject())->setName('third'); 102 | $o31 = (new ExampleObject())->setName('third'); 103 | $o21 = (new ExampleObject())->setName('second'); 104 | $o22 = (new ExampleObject())->setName('second'); 105 | 106 | $map = self::toCollection($o1, $o2, $o3, $o31, $o21, $o22) 107 | ->stream() 108 | ->collect(Collectors::groupByProperty('name')) 109 | ; 110 | 111 | $this->assertEquals(1, $map->get('first')); 112 | $this->assertEquals(3, $map->get('second')); 113 | $this->assertEquals(2, $map->get('third')); 114 | } 115 | } -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Collectors.php: -------------------------------------------------------------------------------- 1 | > 16 | * @package WS\Utils\Collections\Functions 17 | */ 18 | class Collectors 19 | { 20 | /** 21 | * Returns function with interface for concatenating collection strings 22 | * @param string $delimiter 23 | * @return Closure 24 | */ 25 | public static function concat(string $delimiter = ''): Closure 26 | { 27 | return static function (Collection $collection) use ($delimiter) { 28 | return implode($delimiter, $collection->toArray()); 29 | }; 30 | } 31 | 32 | /** 33 | * Returns closure for getting average collection value 34 | * @return Closure 35 | */ 36 | public static function average(): Closure 37 | { 38 | /** 39 | * @param Collection $collection 40 | * @return float|int 41 | */ 42 | return static function (Collection $collection) { 43 | $array = $collection->toArray(); 44 | 45 | return array_sum($array) / count($array); 46 | }; 47 | } 48 | 49 | /** 50 | * Returns closure for getting >. keys - collection uniq values, values - count of repeats 51 | * @return Closure 52 | */ 53 | public static function group(): Closure 54 | { 55 | return static function (Collection $collection): Map { 56 | $groupBy = self::groupBy(static function ($el) { 57 | return $el; 58 | }); 59 | return $groupBy($collection); 60 | }; 61 | } 62 | 63 | /** 64 | * Returns closure for getting map >. keys - value uniq fieldName values, values - count of repeats 65 | * @param string $property 66 | * @return Closure 67 | */ 68 | public static function groupByProperty(string $property): Closure 69 | { 70 | return static function (Collection $collection) use ($property): Map { 71 | $fGetValue = static function ($obj) use ($property) { 72 | return ObjectFunctions::getPropertyValue($obj, $property); 73 | }; 74 | 75 | $groupBy = self::groupBy($fGetValue); 76 | return $groupBy($collection); 77 | }; 78 | } 79 | 80 | /** 81 | * Returns closure for getting map >. keys - value uniq fieldName values, values - count of repeats 82 | * @param callable $f 83 | * @return Closure 84 | */ 85 | public static function groupBy(callable $f): Closure 86 | { 87 | return static function (Collection $collection) use ($f): Map { 88 | $group = new HashMap(); 89 | $collection 90 | ->stream() 91 | ->each(static function ($el) use ($group, $f) { 92 | $value = $f($el); 93 | $count = 0; 94 | if (($gCount = $group->get($value)) !== null) { 95 | $count = $gCount; 96 | } 97 | $group->put($value, $count + 1); 98 | }); 99 | return $group; 100 | }; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ReorganizersFunctionsTest.php: -------------------------------------------------------------------------------- 1 | stream() 27 | ->reorganize(Reorganizers::chunk(2)) 28 | ->getCollection() 29 | ; 30 | 31 | $this->assertEquals(3, $chunkedCollection->size()); 32 | $this->assertThat($chunkedCollection->stream()->findFirst(), CollectionIsEqual::to([1, 2])); 33 | $this->assertThat($chunkedCollection->stream()->findLast(), CollectionIsEqual::to([5, 6])); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function collapsing(): void 40 | { 41 | $collection = self::toCollection([1, 2], [3, 4], [5, 6]); 42 | 43 | $collapsedCollection = $collection 44 | ->stream() 45 | ->reorganize(Reorganizers::collapse()) 46 | ->getCollection() 47 | ; 48 | $this->assertThat($collapsedCollection, CollectionIsEqual::to([1, 2, 3, 4, 5, 6])); 49 | } 50 | 51 | /** 52 | * @test 53 | */ 54 | public function singleDepthCollapsing(): void 55 | { 56 | $collection = self::toCollection([1, 2], [3, 4], [5, 6, [7, 8]]); 57 | 58 | $collapsedCollection = $collection 59 | ->stream() 60 | ->reorganize(Reorganizers::collapse(1)) 61 | ->getCollection() 62 | ; 63 | $this->assertThat($collapsedCollection, CollectionIsEqual::to([1, 2, 3, 4, 5, 6, [7, 8]])); 64 | } 65 | 66 | /** 67 | * @test 68 | */ 69 | public function numericDepthCollapsing(): void 70 | { 71 | $collection = self::toCollection([1, 2], [3, 4], [5, 6, [7, 8, [9, 10]]]); 72 | 73 | $collapsedCollection = $collection 74 | ->stream() 75 | ->reorganize(Reorganizers::collapse(3)) 76 | ->getCollection() 77 | ; 78 | $this->assertThat($collapsedCollection, CollectionIsEqual::to([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function filterDistinctElements(): void 85 | { 86 | $o1 = new TestInteger(1); 87 | $o2 = new TestInteger(2); 88 | $o3 = new TestInteger(3); 89 | $o4 = new TestInteger(1); 90 | $o5 = new TestInteger(2); 91 | 92 | $uniqCollection = CollectionFactory::from([$o1, $o2, $o3, $o4, $o5]) 93 | ->stream() 94 | ->filter(Predicates::lockDuplicated()) 95 | ->getCollection() 96 | ; 97 | 98 | self::assertThat($uniqCollection, CollectionIsEqual::to([$o1, $o2, $o3])); 99 | } 100 | 101 | /** 102 | * @test 103 | */ 104 | public function filterDistinctCastedValues(): void 105 | { 106 | $caster = static function (int $number) { 107 | return $number % 2; 108 | }; 109 | 110 | $result = CollectionFactory::numbers(0, 10) 111 | ->stream() 112 | ->filter(Predicates::lockDuplicated($caster)) 113 | ->toArray() 114 | ; 115 | 116 | $this->assertCount(2, $result); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/SetInterfaceTestTrait.php: -------------------------------------------------------------------------------- 1 | createInstance(); 23 | 24 | $instance->add(1); 25 | $instance->add(1); 26 | $instance->add(1); 27 | 28 | self::assertEquals(1, $instance->size()); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function uniquenessObjectElements(): void 35 | { 36 | $instance = $this->createInstance(); 37 | 38 | $ob = new SplObjectStorage(); 39 | $instance->add($ob); 40 | $instance->add($ob); 41 | $instance->add($ob); 42 | 43 | self::assertEquals(1, $instance->size()); 44 | } 45 | 46 | /** 47 | * @test 48 | */ 49 | public function uniquenessHashCodeAwareObjects(): void 50 | { 51 | $instance = $this->createInstance(); 52 | 53 | $instance->add(new TestInteger(1)); 54 | $instance->add(new TestInteger(1)); 55 | $instance->add(new TestInteger(1)); 56 | 57 | self::assertEquals(1, $instance->size()); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function equalsSetChecking(): void 64 | { 65 | $instance = $this->createInstance(); 66 | $instance->add(1); 67 | $instance->add(2); 68 | $instance->add(3); 69 | 70 | $anotherInstance = $this->createInstance(); 71 | $anotherInstance->add(3); 72 | $anotherInstance->add(2); 73 | $anotherInstance->add(1); 74 | 75 | self::assertThat($anotherInstance, CollectionIsEqual::to($instance)); 76 | } 77 | 78 | /** 79 | * @test 80 | */ 81 | public function notEqualsSetCheckingRight(): void 82 | { 83 | $instance = $this->createInstance(); 84 | $instance->add(1); 85 | $instance->add(2); 86 | $instance->add(3); 87 | $instance->add(4); 88 | 89 | $anotherInstance = $this->createInstance(); 90 | $anotherInstance->add(3); 91 | $anotherInstance->add(2); 92 | $anotherInstance->add(1); 93 | 94 | self::assertThat($anotherInstance, CollectionIsNotEqual::to($instance)); 95 | } 96 | 97 | /** 98 | * @test 99 | */ 100 | public function notEqualsSetCheckingBack(): void 101 | { 102 | $instance = $this->createInstance(); 103 | $instance->add(1); 104 | $instance->add(2); 105 | $instance->add(3); 106 | 107 | $anotherInstance = $this->createInstance(); 108 | $anotherInstance->add(4); 109 | $anotherInstance->add(3); 110 | $anotherInstance->add(2); 111 | $anotherInstance->add(1); 112 | 113 | self::assertThat($anotherInstance, CollectionIsNotEqual::to($instance)); 114 | } 115 | 116 | /** 117 | * @test 118 | */ 119 | public function notEqualsSetCheckingDiffElements(): void 120 | { 121 | $instance = $this->createInstance(); 122 | $instance->add(1); 123 | $instance->add(2); 124 | $instance->add(3); 125 | 126 | $anotherInstance = $this->createInstance(); 127 | $anotherInstance->add(4); 128 | $anotherInstance->add(2); 129 | $anotherInstance->add(1); 130 | 131 | self::assertThat($anotherInstance, CollectionIsNotEqual::to($instance)); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/DummyStreamDecorator.php: -------------------------------------------------------------------------------- 1 | decoratedStream = $originalStream; 19 | } 20 | 21 | public function each(callable $consumer): Stream 22 | { 23 | return $this; 24 | } 25 | 26 | public function walk(callable $consumer, ?int $limit = null): Stream 27 | { 28 | return $this; 29 | } 30 | 31 | public function filter(callable $predicate): Stream 32 | { 33 | return $this; 34 | } 35 | 36 | public function reorganize(callable $reorganizer): Stream 37 | { 38 | return $this; 39 | } 40 | 41 | public function allMatch(callable $predicate): bool 42 | { 43 | return $this->decoratedStream->allMatch($predicate); 44 | } 45 | 46 | public function anyMatch(callable $predicate): bool 47 | { 48 | return $this->decoratedStream->anyMatch($predicate); 49 | } 50 | 51 | public function map(callable $converter): Stream 52 | { 53 | return $this; 54 | } 55 | 56 | public function collect(callable $collector) 57 | { 58 | return $this->decoratedStream->collect($collector); 59 | } 60 | 61 | public function findAny() 62 | { 63 | return $this->decoratedStream->findAny(); 64 | } 65 | 66 | public function findFirst(callable $filter = null) 67 | { 68 | return $this->decoratedStream->findFirst(); 69 | } 70 | 71 | public function findLast() 72 | { 73 | return $this->decoratedStream->findLast(); 74 | } 75 | 76 | public function min(callable $comparator) 77 | { 78 | return $this->decoratedStream->min($comparator); 79 | } 80 | 81 | public function max(callable $comparator) 82 | { 83 | return $this->decoratedStream->max($comparator); 84 | } 85 | 86 | public function sort(callable $comparator): Stream 87 | { 88 | return $this; 89 | } 90 | 91 | public function sortBy(callable $extractor): Stream 92 | { 93 | return $this; 94 | } 95 | 96 | public function sortDesc(callable $comparator): Stream 97 | { 98 | return $this; 99 | } 100 | 101 | public function sortByDesc(callable $extractor): Stream 102 | { 103 | return $this; 104 | } 105 | 106 | public function reverse(): Stream 107 | { 108 | return $this; 109 | } 110 | 111 | public function reduce(callable $accumulator, $initialValue = null) 112 | { 113 | return $this->decoratedStream->reduce($accumulator, $initialValue); 114 | } 115 | 116 | public function limit(int $size): Stream 117 | { 118 | return $this; 119 | } 120 | 121 | public function getCollection(): Collection 122 | { 123 | return $this->decoratedStream->getCollection(); 124 | } 125 | 126 | public function when(bool $condition): Stream 127 | { 128 | if ($condition) { 129 | return $this->decoratedStream; 130 | } 131 | 132 | return $this; 133 | } 134 | 135 | public function always(): Stream 136 | { 137 | return $this->decoratedStream; 138 | } 139 | 140 | /** 141 | * @inheritDoc 142 | */ 143 | public function toArray(): array 144 | { 145 | return $this 146 | ->getCollection() 147 | ->toArray() 148 | ; 149 | } 150 | 151 | /** 152 | * @inheritDoc 153 | */ 154 | public function getSet(): Set 155 | { 156 | return new HashSet($this->toArray()); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/HashMap.php: -------------------------------------------------------------------------------- 1 | 4 | * @license MIT 5 | */ 6 | 7 | namespace WS\Utils\Collections; 8 | 9 | use ArrayIterator; 10 | use RuntimeException; 11 | 12 | class HashMap implements Map 13 | { 14 | private $entries = []; 15 | 16 | public function put($key, $value): bool 17 | { 18 | $this->entries[$this->getKeyHash($key)] = new MapEntry($key, $value); 19 | 20 | return true; 21 | } 22 | 23 | public function getIterator() 24 | { 25 | return new class($this->entries) extends ArrayIterator { 26 | private $entries; 27 | public function __construct(array $entries) 28 | { 29 | $hashToValueArray = array_map(static function (MapEntry $entry) { 30 | return $entry->getValue(); 31 | }, $entries); 32 | parent::__construct($hashToValueArray); 33 | $this->entries = $entries; 34 | } 35 | 36 | public function key() 37 | { 38 | $arrayKey = parent::key(); 39 | /** @var MapEntry $entry */ 40 | $entry = $this->entries[$arrayKey]; 41 | return $entry->getKey(); 42 | } 43 | }; 44 | } 45 | 46 | public function values(): Collection 47 | { 48 | $values = []; 49 | /** @var MapEntry $entry */ 50 | foreach ($this->entries as $entry) { 51 | $values[] = $entry->getValue(); 52 | } 53 | return new ArrayList($values); 54 | } 55 | 56 | public function keys(): Collection 57 | { 58 | $keys = []; 59 | /** @var MapEntry $entry */ 60 | foreach ($this->entries as $entry) { 61 | $keys[] = $entry->getKey(); 62 | } 63 | return new ArrayList($keys); 64 | } 65 | 66 | /** 67 | * @param $key 68 | * @return string 69 | */ 70 | private function getKeyHash($key): string 71 | { 72 | if (is_scalar($key)) { 73 | return $key.''; 74 | } 75 | if ($key instanceof HashCodeAware) { 76 | return $key->getHashCode(); 77 | } 78 | if (is_object($key)) { 79 | return spl_object_hash($key); 80 | } 81 | if ($key === null) { 82 | return '__NULL__'; 83 | } 84 | if (is_array($key)) { 85 | return md5(json_encode($key)); 86 | } 87 | throw new RuntimeException("The type of $key is not supported"); 88 | } 89 | 90 | public function remove($key): bool 91 | { 92 | $res = $this->containsKey($key); 93 | 94 | if (!$res) { 95 | return false; 96 | } 97 | unset($this->entries[$this->getKeyHash($key)]); 98 | return true; 99 | } 100 | 101 | /** 102 | * @inheritDoc 103 | */ 104 | public function containsKey($key): bool 105 | { 106 | return isset($this->entries[$this->getKeyHash($key)]); 107 | } 108 | 109 | public function size(): int 110 | { 111 | return count($this->entries); 112 | } 113 | 114 | /** 115 | * @inheritDoc 116 | */ 117 | public function get($key) 118 | { 119 | if (!$this->containsKey($key)) { 120 | return null; 121 | } 122 | 123 | $entry = $this->entries[$this->getKeyHash($key)]; 124 | return $entry->getValue(); 125 | } 126 | 127 | /** 128 | * @inheritDoc 129 | */ 130 | public function containsValue($tested): bool 131 | { 132 | foreach ($this->entries as $entry) { 133 | if ($entry->getValue() === $tested) { 134 | return true; 135 | } 136 | } 137 | return false; 138 | } 139 | 140 | public function stream(): Stream 141 | { 142 | return new SerialStream(CollectionFactory::from(array_values($this->entries))); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ArrayListTest.php: -------------------------------------------------------------------------------- 1 | createInstance(1, 2, 3); 28 | 29 | self::assertEquals(1, $list->get(0)); 30 | self::assertEquals(2, $list->get(1)); 31 | self::assertEquals(3, $list->get(2)); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function settingAtIndex(): void 38 | { 39 | $list = $this->createInstance(1, 2, 3); 40 | 41 | $list->set(4, 1); 42 | 43 | self::assertEquals(1, $list->get(0)); 44 | self::assertEquals(4, $list->get(1)); 45 | self::assertEquals(3, $list->get(2)); 46 | 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function settingIndexIntoOutOfRange(): void 53 | { 54 | $list = $this->createInstance(1, 2, 3); 55 | 56 | $this->expectException(OutOfRangeException::class); 57 | $list->set(4, 3); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function gettingIndexOf(): void 64 | { 65 | $list = $this->createInstance(1, 2, 3); 66 | 67 | self::assertEquals(0, $list->indexOf(1)); 68 | self::assertEquals(1, $list->indexOf(2)); 69 | self::assertEquals(2, $list->indexOf(3)); 70 | } 71 | 72 | /** 73 | * @test 74 | */ 75 | public function indexOfObjectGetting(): void 76 | { 77 | $i1 = new TestInteger(1); 78 | $i2 = new TestInteger(2); 79 | $i3 = new TestInteger(3); 80 | 81 | $list = $this->createInstance($i1, $i2, $i3); 82 | 83 | self::assertEquals(0, $list->indexOf($i1)); 84 | self::assertEquals(1, $list->indexOf($i2)); 85 | self::assertEquals(2, $list->indexOf($i3)); 86 | } 87 | 88 | /** 89 | * @test 90 | */ 91 | public function lastIndexOfElementGetting(): void 92 | { 93 | $i0 = new TestInteger(0); 94 | $i1 = new TestInteger(1); 95 | $i2 = new TestInteger(2); 96 | $i3 = new TestInteger(3); 97 | 98 | $list = $this->createInstance($i0, $i1, $i2, $i3, $i2, $i1); 99 | 100 | self::assertEquals(4, $list->lastIndexOf($i2)); 101 | self::assertEquals(5, $list->lastIndexOf($i1)); 102 | self::assertEquals(3, $list->lastIndexOf($i3)); 103 | self::assertEquals(0, $list->lastIndexOf($i0)); 104 | } 105 | 106 | /** 107 | * @test 108 | */ 109 | public function lastIndexGettingOfEmptyCollection(): void 110 | { 111 | $list = $this->createInstance(); 112 | 113 | $res = $list->lastIndexOf(10); 114 | 115 | self::assertNull($res); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function removingAtPositionElement(): void 122 | { 123 | $list = $this->createInstance(1, 2, 3); 124 | 125 | $el = $list->removeAt(0); 126 | 127 | self::assertEquals(1, $el); 128 | self::assertEquals(2, $list->get(0)); 129 | self::assertEquals(2, $list->size()); 130 | } 131 | 132 | /** 133 | * @test 134 | */ 135 | public function removingAtEmptyCollection(): void 136 | { 137 | $list = $this->createInstance(); 138 | 139 | $res = $list->removeAt(10); 140 | 141 | self::assertNull($res); 142 | } 143 | 144 | /** 145 | * @test 146 | */ 147 | public function removingWithoutGaps(): void 148 | { 149 | $list = $this->createInstance(1, 2, 3, 4); 150 | 151 | $list->remove(2); 152 | 153 | self::assertEquals(3, $list->size()); 154 | self::assertEquals(3, $list->get(1)); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ArrayStackTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(3, $stack->peek()); 28 | $this->assertEquals(3, $stack->size()); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function peekingEmpty(): void 35 | { 36 | $this->expectException(RuntimeException::class); 37 | 38 | $stack = new ArrayStack(); 39 | $stack->peek(); 40 | } 41 | 42 | /** 43 | * @test 44 | */ 45 | public function pushing(): void 46 | { 47 | $stack = ArrayStack::of(100, 0, 50); 48 | 49 | $this->assertTrue($stack->push(10)); 50 | $this->assertTrue($stack->push(10)); 51 | $this->assertEquals(5, $stack->size()); 52 | $this->assertTrue($stack->push(15)); 53 | $this->assertEquals(15, $stack->peek()); 54 | $this->assertEquals(6, $stack->size()); 55 | } 56 | 57 | /** 58 | * @test 59 | */ 60 | public function popping(): void 61 | { 62 | $stack = ArrayStack::of(100, 0, '50', 'asd', 20); 63 | 64 | $this->assertEquals(20, $stack->pop()); 65 | $this->assertEquals('asd', $stack->pop()); 66 | $this->assertEquals('50', $stack->pop()); 67 | $this->assertEquals(0, $stack->pop()); 68 | $this->assertEquals(100, $stack->pop()); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function emptyPopping(): void 75 | { 76 | $this->expectException(RuntimeException::class); 77 | 78 | $stack = new ArrayStack(); 79 | $stack->pop(); 80 | } 81 | 82 | /** 83 | * @test 84 | */ 85 | public function iterating(): void 86 | { 87 | $stack = ArrayStack::of(9, 8, 'a', 'n'); 88 | 89 | $elements = []; 90 | /** @noinspection PhpUnhandledExceptionInspection */ 91 | foreach ($stack->getIterator() as $element) { 92 | $elements[] = $element; 93 | } 94 | $this->assertEquals(['n', 'a', 8, 9], $elements); 95 | 96 | $elements = []; 97 | /** @noinspection PhpUnhandledExceptionInspection */ 98 | foreach ($stack->getIterator() as $element) { 99 | $elements[] = $element; 100 | } 101 | $this->assertEquals(['n', 'a', 8, 9], $elements); 102 | } 103 | 104 | /** 105 | * @test 106 | */ 107 | public function merging(): void 108 | { 109 | $stack1 = ArrayStack::of(1, 7); 110 | $stack2 = ArrayStack::of('c', 'a'); 111 | 112 | $stack1->merge($stack2); 113 | $this->assertEquals(['c', 'a', 7, 1], $stack1->toArray()); 114 | 115 | $stack2->merge($stack2); 116 | $this->assertEquals(['c', 'a', 'a', 'c'], $stack2->toArray()); 117 | } 118 | 119 | /** 120 | * @test 121 | */ 122 | public function equivalency(): void 123 | { 124 | $stack1 = ArrayStack::of(1, 7); 125 | $stack2 = ArrayStack::of(1, 7); 126 | 127 | $this->assertTrue($stack1->equals($stack2)); 128 | $this->assertTrue($stack2->equals($stack1)); 129 | } 130 | 131 | /** 132 | * @test 133 | */ 134 | public function absentEquivalency(): void 135 | { 136 | $stack1 = ArrayStack::of(1, 7); 137 | $stack2 = ArrayStack::of(7, 1); 138 | $stack3 = ArrayStack::of(1, 7, 7); 139 | $stack4 = ArrayStack::of('1', '7'); 140 | 141 | $this->assertFalse($stack1->equals($stack2)); 142 | $this->assertFalse($stack1->equals($stack3)); 143 | $this->assertFalse($stack1->equals($stack4)); 144 | $this->assertFalse($stack2->equals($stack3)); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ComparatorsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($comparator($a, $b), $expected); 42 | } 43 | 44 | public function objectComparatorTestCases(): array 45 | { 46 | $sequence = []; 47 | 48 | for ($i = 0; $i < 10; $i++) { 49 | $obj = new ExampleObject(); 50 | $sequence[] = $obj; 51 | $obj->property = $i; 52 | $obj->setField($i); 53 | $obj->setName($i); 54 | } 55 | 56 | $cases = []; 57 | for ($i = 0; $i < 2; $i++) { 58 | $shuffledSequence = (new ArrayList($sequence)) 59 | ->stream() 60 | ->collect(Reorganizers::shuffle()) 61 | ->toArray() 62 | ; 63 | $cases[] = [$shuffledSequence, 'property', $sequence]; 64 | } 65 | for ($i = 0; $i < 2; $i++) { 66 | $shuffledSequence = (new ArrayList($sequence)) 67 | ->stream() 68 | ->collect(Reorganizers::shuffle()) 69 | ->toArray() 70 | ; 71 | $cases[] = [$shuffledSequence, 'name', $sequence]; 72 | } 73 | for ($i = 0; $i < 2; $i++) { 74 | $shuffledSequence = (new ArrayList($sequence)) 75 | ->stream() 76 | ->collect(Reorganizers::shuffle()) 77 | ->toArray() 78 | ; 79 | $cases[] = [$shuffledSequence, 'field', $sequence]; 80 | } 81 | 82 | return $cases; 83 | } 84 | 85 | /** 86 | * @dataProvider objectComparatorTestCases 87 | * @test 88 | * @param $sequence 89 | * @param $field 90 | * @param $expected 91 | */ 92 | public function objectComparatorChecking($sequence, $field, $expected): void 93 | { 94 | $actual = (new ArrayList($sequence)) 95 | ->stream() 96 | ->sort(Comparators::objectPropertyComparator($field)) 97 | ->getCollection() 98 | ->toArray(); 99 | 100 | $this->assertEquals($expected, $actual); 101 | } 102 | 103 | /** 104 | * @dataProvider scalarComparatorCases 105 | * @test 106 | * @param $a 107 | * @param $b 108 | * @param $expected 109 | */ 110 | public function callbackComparatorChecking($a, $b, $expected): void 111 | { 112 | $f = new InvokeCounter(static function ($value) { 113 | return $value; 114 | }); 115 | $comparator = Comparators::callbackComparator($f); 116 | $this->assertEquals($comparator($a, $b), $expected); 117 | $this->assertTrue($f->countOfInvokes() > 0); 118 | } 119 | 120 | /** 121 | * @test 122 | */ 123 | public function integrateCallbackComparatorChecking(): void 124 | { 125 | $f = new InvokeCounter(static function ($value) { 126 | return $value; 127 | }); 128 | 129 | $sourceCollection = CollectionFactory::numbers(10); 130 | $shuffledCollection = $sourceCollection 131 | ->stream() 132 | ->reorganize(Reorganizers::shuffle()) 133 | ->getCollection(); 134 | 135 | $this->assertThat($sourceCollection, CollectionIsNotEqual::to($shuffledCollection)); 136 | 137 | $sortedCollection = $shuffledCollection->stream() 138 | ->sort(Comparators::callbackComparator($f)) 139 | ->getCollection() 140 | ; 141 | $this->assertThat($sourceCollection, CollectionIsEqual::to($sortedCollection)); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Reorganizers.php: -------------------------------------------------------------------------------- 1 | that shuffles elements 20 | * @return Closure 21 | */ 22 | public static function shuffle(): Closure 23 | { 24 | return static function (Collection $collection): Collection { 25 | $array = $collection->toArray(); 26 | shuffle($array); 27 | 28 | return self::collectionConstructor($array); 29 | }; 30 | } 31 | 32 | /** 33 | * Returns Closure that gets $count random elements from collection 34 | * @param int $count 35 | * @return Closure 36 | */ 37 | public static function random(int $count = 1): Closure 38 | { 39 | return static function (Collection $collection) use ($count): Collection { 40 | /** 41 | * Collection 42 | */ 43 | $resultCollection = self::collectionConstructor(); 44 | if ($count === 0) { 45 | return $resultCollection; 46 | } 47 | 48 | $collectionSize = $collection->size(); 49 | $expectedCount = $count; 50 | 51 | if ($collectionSize < $expectedCount) { 52 | return $resultCollection; 53 | } 54 | 55 | $rest = $collectionSize; 56 | $generated = 0; 57 | $multiplicity = (int)round($collectionSize / $expectedCount); 58 | 59 | $rangeRandomizer = static function () use ($multiplicity): int { 60 | return random_int(0, $multiplicity - 1); 61 | }; 62 | 63 | $trier = static function () use (& $generated, & $rest, $expectedCount, $rangeRandomizer): bool { 64 | $rest--; 65 | if ($generated === $expectedCount) { 66 | return false; 67 | } 68 | if ($generated + $rest + 1 <= $expectedCount) { 69 | return true; 70 | } 71 | if ($rangeRandomizer() !== 0) { 72 | return false; 73 | } 74 | $generated++; 75 | return true; 76 | }; 77 | 78 | return $collection 79 | ->stream() 80 | ->filter($trier) 81 | ->getCollection(); 82 | }; 83 | } 84 | 85 | /** 86 | * Returns Closure that split collection into sub collections with $size 87 | * @param int $size 88 | * @return Closure 89 | */ 90 | public static function chunk(int $size): Closure 91 | { 92 | return static function (Collection $collection) use ($size): Collection { 93 | $chunkCollection = self::collectionConstructor(); 94 | $currentChunk = self::collectionConstructor(); 95 | $pointer = $size; 96 | $collection 97 | ->stream() 98 | ->each(static function ($el) use ($size, $chunkCollection, & $currentChunk, & $pointer) { 99 | $pointer--; 100 | $currentChunk->add($el); 101 | 102 | if ($pointer === 0) { 103 | $chunkCollection->add($currentChunk); 104 | $currentChunk = self::collectionConstructor(); 105 | $pointer = $size; 106 | } 107 | }) 108 | ; 109 | return $chunkCollection; 110 | }; 111 | } 112 | 113 | /** 114 | * Returns Closure that collapses a collection of arrays into a single, flat collection 115 | * @param int $depth Depth of collapses. The 0 value is without 116 | * @return Closure 117 | */ 118 | public static function collapse(int $depth = 0): Closure 119 | { 120 | if ($depth === 0) { 121 | $depth = null; 122 | } 123 | return static function (Collection $collection) use ($depth): Collection { 124 | $flatIterable = static function (iterable $collection, $depth) use (& $flatIterable): array { 125 | $goToDepth = $depth > 0 || $depth === null; 126 | if ($depth !== null) { 127 | $depth--; 128 | } 129 | 130 | $res = []; 131 | foreach ($collection as $item) { 132 | if (is_iterable($item) && $goToDepth) { 133 | $toPush = $flatIterable($item, $depth); 134 | array_unshift($toPush, $res); 135 | array_push(...$toPush); 136 | $res = array_shift($toPush); 137 | } else { 138 | $res[] = $item; 139 | } 140 | } 141 | return $res; 142 | }; 143 | 144 | return self::collectionConstructor($flatIterable($collection, $depth)); 145 | }; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Stream.php: -------------------------------------------------------------------------------- 1 | 108 | * @return Stream 109 | */ 110 | public function sortBy(callable $extractor): Stream; 111 | 112 | /** 113 | * @param callable $comparator 114 | * @return Stream 115 | */ 116 | public function sortDesc(callable $comparator): Stream; 117 | 118 | /** 119 | * Sorts desc elements with value extractor via common scalar sort method 120 | * @param callable $extractor function for getting value 121 | * @return Stream 122 | */ 123 | public function sortByDesc(callable $extractor): Stream; 124 | 125 | /** 126 | * Placed elements in reverse order 127 | * @return Stream 128 | */ 129 | public function reverse(): Stream; 130 | 131 | /** 132 | * Reduce collection to single value with accumulator 133 | * @param callable $accumulator 134 | * @param mixed|null $initialValue 135 | * @return mixed 136 | */ 137 | public function reduce(callable $accumulator, $initialValue = null); 138 | 139 | /** 140 | * Limits amount of stream collection elements 141 | * @param int $size 142 | * @return Stream 143 | */ 144 | public function limit(int $size): Stream; 145 | 146 | /** 147 | * If condition is false stream became inert with stream functions and if next call will be true stream became operated again 148 | * @param bool $condition 149 | * @return Stream 150 | */ 151 | public function when(bool $condition): Stream; 152 | 153 | /** 154 | * Returns collection 155 | * @return Collection 156 | */ 157 | public function getCollection(): Collection; 158 | 159 | /** 160 | * Clear current condition. The same as when(true) 161 | * @return Stream 162 | */ 163 | public function always(): Stream; 164 | 165 | /** 166 | * Returns array as collection represent 167 | * @return array 168 | */ 169 | public function toArray(): array; 170 | 171 | /** 172 | * Creates a set structure 173 | * @return Set 174 | */ 175 | public function getSet(): Set; 176 | } 177 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Group/Group.php: -------------------------------------------------------------------------------- 1 | 4 | */ 5 | 6 | namespace WS\Utils\Collections\Functions\Group; 7 | 8 | use WS\Utils\Collections\Collection; 9 | use WS\Utils\Collections\CollectionFactory; 10 | use WS\Utils\Collections\Functions\ObjectFunctions; 11 | 12 | class Group 13 | { 14 | 15 | private $key; 16 | private $aggregators; 17 | 18 | public function __construct(string $key) 19 | { 20 | $this->key = $key; 21 | } 22 | 23 | public function __invoke(Collection $collection): array 24 | { 25 | $groupedResult = $this->group($collection); 26 | if (!$this->aggregators) { 27 | return $groupedResult; 28 | } 29 | return $this->applyAggregators($groupedResult); 30 | } 31 | 32 | private function group(Collection $collection): array 33 | { 34 | $result = []; 35 | foreach ($collection as $element) { 36 | if (!$groupKey = ObjectFunctions::getPropertyValue($element, $this->key)) { 37 | continue; 38 | } 39 | if (!isset($result[$groupKey])) { 40 | $result[$groupKey] = CollectionFactory::empty(); 41 | } 42 | $result[$groupKey]->add($element); 43 | } 44 | return $result; 45 | } 46 | 47 | private function applyAggregators(array $groupedResult): array 48 | { 49 | $result = []; 50 | foreach ($groupedResult as $groupKey => $items) { 51 | foreach ($this->aggregators as $item) { 52 | [$destKey, $aggregator] = $item; 53 | $result[$groupKey][$destKey] = $aggregator($items); 54 | } 55 | } 56 | return $result; 57 | } 58 | 59 | /** 60 | * Create new instance of Group and use $key as group key 61 | * @param string $key 62 | * @return static 63 | */ 64 | public static function by(string $key): self 65 | { 66 | return new self($key); 67 | } 68 | 69 | /** 70 | * Will calculate sum of items based on $sourceKey and put it to $destKey 71 | * @param string $sourceKey 72 | * @param string|null $destKey 73 | * @return $this 74 | */ 75 | public function sum(string $sourceKey, string $destKey = null): self 76 | { 77 | return $this->addAggregator($destKey ?? $sourceKey, new Aggregator\Sum($sourceKey)); 78 | } 79 | 80 | /** 81 | * Will calculate minimum value of items based on $sourceKey and put it to $destKey 82 | * @param string $sourceKey 83 | * @param string|null $destKey 84 | * @return $this 85 | */ 86 | public function min(string $sourceKey, string $destKey = null): self 87 | { 88 | return $this->addAggregator($destKey ?? $sourceKey, new Aggregator\Min($sourceKey)); 89 | } 90 | 91 | /** 92 | * Will calculate maximum value of items based on $sourceKey and put it to $destKey 93 | * @param string $sourceKey 94 | * @param string|null $destKey 95 | * @return $this 96 | */ 97 | public function max(string $sourceKey, string $destKey = null): self 98 | { 99 | return $this->addAggregator($destKey ?? $sourceKey, new Aggregator\Max($sourceKey)); 100 | } 101 | 102 | /** 103 | * Will calculate average value of items based on $sourceKey and put it to $destKey 104 | * @param string $sourceKey 105 | * @param string|null $destKey 106 | * @return $this 107 | */ 108 | public function avg(string $sourceKey, string $destKey = null): self 109 | { 110 | return $this->addAggregator($destKey ?? $sourceKey, new Aggregator\Avg($sourceKey)); 111 | } 112 | 113 | /** 114 | * Will find unique values of items based on $sourceKey and put it to $destKey 115 | * @param string $sourceKey 116 | * @param string|null $destKey 117 | * @return $this 118 | */ 119 | public function addToSet(string $sourceKey, string $destKey = null): self 120 | { 121 | return $this->addAggregator($destKey ?? $sourceKey, new Aggregator\AddToSet($sourceKey)); 122 | } 123 | 124 | /** 125 | * Will return first value of items based on $sourceKey and put it to $destKey 126 | * @param string $sourceKey 127 | * @param string|null $destKey 128 | * @return $this 129 | */ 130 | public function first(string $sourceKey, string $destKey = null): self 131 | { 132 | return $this->addAggregator($destKey ?? $sourceKey, new Aggregator\First($sourceKey)); 133 | } 134 | 135 | /** 136 | * Will return last value of items based on $sourceKey and put it to $destKey 137 | * @param string $sourceKey 138 | * @param string|null $destKey 139 | * @return $this 140 | */ 141 | public function last(string $sourceKey, string $destKey = null): self 142 | { 143 | return $this->addAggregator($destKey ?? $sourceKey, new Aggregator\Last($sourceKey)); 144 | } 145 | 146 | /** 147 | * Will calculate count of items in group and put it to $destKey 148 | * @param string $destKey 149 | * @return $this 150 | */ 151 | public function count(string $destKey): self 152 | { 153 | return $this->addAggregator($destKey, new Aggregator\Count()); 154 | } 155 | 156 | /** 157 | * Add custom $aggregator with interface Aggregator\Aggregator| 158 | * @param string $destKey 159 | * @param callable $aggregator 160 | * @return $this 161 | */ 162 | public function addAggregator(string $destKey, callable $aggregator): self 163 | { 164 | $this->aggregators[] = [$destKey, $aggregator]; 165 | return $this; 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/ConditionsStreamTest.php: -------------------------------------------------------------------------------- 1 | stream() 25 | ->when(false) 26 | ->filter(Predicates::lessThan(4)) 27 | ->getCollection() 28 | ; 29 | self::assertEquals(10, $collection->size()); 30 | } 31 | 32 | /** 33 | * @test 34 | */ 35 | public function keepingOriginalStream(): void 36 | { 37 | $collection = CollectionFactory::numbers(10) 38 | ->stream() 39 | ->always() 40 | ->filter(Predicates::eachEven()) 41 | ->getCollection() 42 | ; 43 | 44 | self::assertEquals(5, $collection->size()); 45 | } 46 | 47 | /** 48 | * @test 49 | */ 50 | public function obtainNormalStreamFromDummy(): void 51 | { 52 | $collection = CollectionFactory::numbers(10) 53 | ->stream() 54 | ->when(false) 55 | ->filter(Predicates::lessOrEqual(5)) 56 | ->when(true) 57 | ->filter(Predicates::greaterOrEqual(5)) 58 | ->getCollection() 59 | ; 60 | 61 | self::assertThat($collection, CollectionIsEqual::to([5, 6, 7, 8, 9])); 62 | } 63 | 64 | /** 65 | * @test 66 | */ 67 | public function obtainNormalStreamWithAlwaysCondition(): void 68 | { 69 | $collection = CollectionFactory::numbers(10) 70 | ->stream() 71 | ->when(false) 72 | ->filter(Predicates::lessOrEqual(5)) 73 | ->always() 74 | ->filter(Predicates::greaterOrEqual(5)) 75 | ->getCollection() 76 | ; 77 | 78 | self::assertThat($collection, CollectionIsEqual::to([5, 6, 7, 8, 9])); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function dontObtainNormalStreamFromDummy(): void 85 | { 86 | $sourceCollection = CollectionFactory::numbers(10); 87 | $resultCollection = $sourceCollection 88 | ->stream() 89 | ->when(false) 90 | ->filter(Predicates::lessOrEqual(5)) 91 | ->when(false) 92 | ->filter(Predicates::greaterOrEqual(5)) 93 | ->getCollection() 94 | ; 95 | 96 | self::assertThat($resultCollection, CollectionIsEqual::to($sourceCollection)); 97 | } 98 | 99 | /** 100 | * @test 101 | */ 102 | public function usingWithoutDummyDecorator(): void 103 | { 104 | $collection = CollectionFactory::numbers(10) 105 | ->stream() 106 | ->when(true) 107 | ->filter(Predicates::lessOrEqual(6)) 108 | ->when(true) 109 | ->filter(Predicates::greaterOrEqual(4)) 110 | ->getCollection() 111 | ; 112 | 113 | self::assertThat($collection, CollectionIsEqual::to([4, 5, 6])); 114 | } 115 | 116 | public function streamModifiers(): array 117 | { 118 | return [ 119 | ['each', new InvokeCounter()], 120 | ['walk', new InvokeCounter()], 121 | ['filter', new InvokeCounter()], 122 | ['reorganize', new InvokeCounter()], 123 | ['map', new InvokeCounter()], 124 | ['sort', new InvokeCounter()], 125 | ['sortBy', new InvokeCounter()], 126 | ['sortDesc', new InvokeCounter()], 127 | ['sortByDesc', new InvokeCounter()], 128 | ['reverse'], 129 | ['limit', 2] 130 | ]; 131 | } 132 | 133 | /** 134 | * @dataProvider streamModifiers 135 | * @test 136 | * @param $method 137 | * @param mixed ...$args 138 | */ 139 | public function shouldNotModifyStream($method, ...$args): void 140 | { 141 | $stream = CollectionFactory::numbers(10) 142 | ->stream() 143 | ->when(false) 144 | ; 145 | $sourceCollection = $stream 146 | ->getCollection(); 147 | 148 | call_user_func_array([$stream, $method], $args); 149 | 150 | foreach ($args as $arg) { 151 | if ($arg instanceof InvokeCounter && $arg->countOfInvokes() > 0) { 152 | self::fail("Modifier callback shouldn't be called"); 153 | } 154 | } 155 | 156 | self::assertThat($sourceCollection, CollectionIsEqual::to($stream->getCollection())); 157 | } 158 | 159 | public function dummyStreamWrapperMethods(): array 160 | { 161 | $f = static function() {}; 162 | return [ 163 | ['allMatch', $f], 164 | ['anyMatch', $f], 165 | ['collect', $f], 166 | ['findAny', $f], 167 | ['findFirst', $f], 168 | ['findLast', $f], 169 | ['min', $f], 170 | ['max', $f], 171 | ['reduce', $f], 172 | ['getCollection'] 173 | ]; 174 | } 175 | 176 | /** 177 | * @dataProvider dummyStreamWrapperMethods 178 | * @test 179 | * @param $method 180 | * @param array $args 181 | */ 182 | public function dummyStreamAsWrapper($method, ...$args): void 183 | { 184 | /** @var Stream|MockObject $mockStream */ 185 | $mockStream = $this->getMockBuilder(Stream::class) 186 | ->getMock() 187 | ; 188 | 189 | $mockStream 190 | ->expects(new InvokedCount(1)) 191 | ->method($method) 192 | ; 193 | 194 | $dummyStream = new DummyStreamDecorator($mockStream); 195 | call_user_func_array([$dummyStream, $method], $args); 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/MapInterfaceTestTrait.php: -------------------------------------------------------------------------------- 1 | createInstance(); 27 | $instance->put($obj1, null); 28 | self::assertTrue($instance->containsKey($obj2)); 29 | 30 | self::assertNull($instance->get($obj2)); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function checkCount(): void 37 | { 38 | $instance = $this->createInstance(); 39 | $instance->put(1,1); 40 | $instance->put(2,1); 41 | $instance->put(2,1); 42 | $instance->put(3,1); 43 | $instance->put(4,1); 44 | $instance->put(5,1); 45 | 46 | $instance->remove(4); 47 | 48 | self::assertEquals(4, $instance->size()); 49 | } 50 | 51 | /** 52 | * @test 53 | */ 54 | public function iterate(): void 55 | { 56 | $instance = $this->createInstance(); 57 | for ($i = 10; $i < 20; $i++) { 58 | $instance->put($i, $i+1); 59 | } 60 | 61 | $i = 10; 62 | foreach ($instance as $k => $v) { 63 | self::assertSame($k, $v - 1); 64 | self::assertSame($i, $k); 65 | $i++; 66 | } 67 | } 68 | 69 | /** 70 | * @test 71 | */ 72 | public function keySetGetting(): void 73 | { 74 | $instance = $this->createInstance(); 75 | $instance->put(1,1); 76 | $instance->put(2,1); 77 | $instance->put(2,1); 78 | $instance->put(3,1); 79 | $instance->put(4,1); 80 | $instance->put(null,1); 81 | 82 | $instance->remove(4); 83 | 84 | $set = $instance->keys(); 85 | 86 | self::assertEquals(4, $set->size()); 87 | } 88 | 89 | /** 90 | * @test 91 | */ 92 | public function valuesGetting(): void 93 | { 94 | $instance = $this->createInstance(); 95 | $instance->put(1,1); 96 | $instance->put(2,1); 97 | $instance->put(2,1); 98 | $instance->put(3,1); 99 | $instance->put(4,1); 100 | $instance->put(5,1); 101 | 102 | $instance->remove(4); 103 | 104 | $values = $instance->values(); 105 | 106 | self::assertEquals(4, $values->size()); 107 | } 108 | 109 | /** 110 | * @test 111 | */ 112 | public function hashCodeAwareChecking(): void 113 | { 114 | $o1 = new TestInteger(1); 115 | $o2 = new TestInteger(2); 116 | $o3 = new TestInteger(3); 117 | 118 | $instance = $this->createInstance(); 119 | $instance->put($o1, null); 120 | $instance->put($o2, null); 121 | $instance->put($o3, null); 122 | 123 | self::assertTrue($instance->containsKey(new TestInteger(1))); 124 | self::assertTrue($instance->containsKey(new TestInteger(2))); 125 | self::assertTrue($instance->containsKey(new TestInteger(3))); 126 | } 127 | 128 | /** 129 | * @test 130 | */ 131 | public function containsValueChecking(): void 132 | { 133 | $instance = $this->createInstance(); 134 | $instance->put(1, 1); 135 | $instance->put(2, 2); 136 | $instance->put(3, 3); 137 | 138 | self::assertTrue($instance->containsValue(1)); 139 | self::assertFalse($instance->containsValue(4)); 140 | } 141 | 142 | /** 143 | * @test 144 | */ 145 | public function getting(): void 146 | { 147 | $instance = $this->createInstance(); 148 | $instance->put(1, 1); 149 | $instance->put(2, 2); 150 | $instance->put(3, 3); 151 | 152 | self::assertNotNull($instance->get(1)); 153 | self::assertNull($instance->get(4)); 154 | } 155 | 156 | /** 157 | * @test 158 | */ 159 | public function usingSimpleArrayAsKey(): void 160 | { 161 | $instance = $this->createInstance(); 162 | $instance->put([1, 2, 3], 1); 163 | $instance->put([1, 2, 3], 2); 164 | $instance->put([1, 3, 3], 3); 165 | 166 | self::assertEquals(2, $instance->size()); 167 | self::assertTrue($instance->containsKey([1, 2, 3])); 168 | self::assertTrue($instance->containsKey([1, 3, 3])); 169 | } 170 | 171 | /** 172 | * @test 173 | */ 174 | public function removingOfAbsent(): void 175 | { 176 | $instance = $this->createInstance(); 177 | $instance->put(1, 1); 178 | $instance->put(2, 2); 179 | $instance->put(3, 3); 180 | 181 | self::assertFalse($instance->remove(4)); 182 | } 183 | 184 | /** 185 | * @test 186 | */ 187 | public function usingFunctionAsKey(): void 188 | { 189 | $instance = $this->createInstance(); 190 | $instance->put(static function () {}, null); 191 | self::assertFalse($instance->containsKey(static function () {})); 192 | } 193 | 194 | /** 195 | * @test 196 | */ 197 | public function unsupportedKeyType(): void 198 | { 199 | $this->expectException(Exception::class); 200 | $map = $this->createInstance(); 201 | $f = null; 202 | try { 203 | $f = fopen(__FILE__, 'rb'); 204 | $map->put($f, null); 205 | } catch (Exception $exception) { 206 | fclose($f); 207 | /** @noinspection PhpUnhandledExceptionInspection */ 208 | throw $exception; 209 | } 210 | } 211 | 212 | /** 213 | * @test 214 | */ 215 | public function foreachObjectKeyValueChecking(): void 216 | { 217 | $map = $this->createInstance(); 218 | 219 | $map->put(new SplObjectStorage(), 1); 220 | $map->put(new SplObjectStorage(), 2); 221 | $map->put(new SplObjectStorage(), 3); 222 | 223 | foreach ($map as $splObjectStorage => $intValue) { 224 | self::assertThat($splObjectStorage, self::isInstanceOf(SplObjectStorage::class)); 225 | } 226 | } 227 | 228 | /** 229 | * @test 230 | */ 231 | public function foreachAnyKeyValueChecking(): void 232 | { 233 | $map = $this->createInstance(); 234 | 235 | $map->put(null, 1); 236 | $map->put(false, 2); 237 | $map->put(true, 3); 238 | 239 | foreach ($map as $k => $int) { 240 | switch ($int) { 241 | case 1: 242 | self::assertThat($k, self::isNull()); 243 | break; 244 | case 2: 245 | self::assertThat($k, self::isFalse()); 246 | break; 247 | case 3: 248 | self::assertThat($k, self::isTrue()); 249 | break; 250 | } 251 | } 252 | } 253 | 254 | /** 255 | * @test 256 | */ 257 | public function streamGetting(): void 258 | { 259 | $map = $this->createInstance(); 260 | 261 | $map->put('1', 1); 262 | $map->put('2', 2); 263 | $map->put('3', 3); 264 | 265 | $stream = $map->stream(); 266 | self::assertThat($stream, self::isInstanceOf(Stream::class)); 267 | 268 | self::assertGreaterThan(0, $stream->getCollection()->size()); 269 | 270 | $stream 271 | ->each(static function (MapEntry $mapEntry) { 272 | self::assertThat($mapEntry->getKey() === ''.$mapEntry->getValue(), self::isTrue()); 273 | }); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/CollectionInterfaceTestTrait.php: -------------------------------------------------------------------------------- 1 | createInstance(1, 2); 20 | $this->assertEquals(2, $instance->size()); 21 | 22 | $this->assertTrue($instance->add(-76)); 23 | $this->assertEquals(3, $instance->size()); 24 | 25 | $anotherInstance = $this->createInstance(); 26 | $this->assertTrue($anotherInstance->add('string')); 27 | $anotherInstance->merge($instance); 28 | $this->assertEquals(4, $anotherInstance->size()); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function merging(): void 35 | { 36 | /** @var Collection $collection */ 37 | $collection = $this->createInstance(1, 2); 38 | 39 | /** @var Collection $anotherCollection */ 40 | $anotherCollection = $this->createInstance(3, 4, 5); 41 | 42 | $clonedCollection = clone $collection; 43 | $collection->merge($anotherCollection); 44 | $this->assertThat($collection, CollectionContainsSameElements::with([1, 2, 3, 4, 5])); 45 | $anotherCollection->merge($clonedCollection); 46 | $this->assertThat($anotherCollection, CollectionContainsSameElements::with([3, 4, 5, 1, 2])); 47 | } 48 | 49 | /** 50 | * @test 51 | */ 52 | public function clearing(): void 53 | { 54 | /** @var Collection $collection */ 55 | $collection = $this->createInstance(27, 'string'); 56 | $collection->clear(); 57 | $this->assertEquals(0, $collection->size()); 58 | } 59 | 60 | /** 61 | * @test 62 | */ 63 | public function removing(): void 64 | { 65 | /** @var Collection $collection */ 66 | $collection = $this->createInstance(27, 'string', -11, 50); 67 | 68 | $this->assertTrue($collection->remove(-11)); 69 | $this->assertEquals(3, $collection->size()); 70 | $this->assertThat($collection, CollectionContainsSameElements::with([27, 'string', 50])); 71 | 72 | $this->assertTrue($collection->remove('string')); 73 | $this->assertEquals(2, $collection->size()); 74 | $this->assertThat($collection, CollectionContainsSameElements::with([27, 50])); 75 | 76 | $this->assertFalse($collection->remove(89)); 77 | $this->assertEquals(2, $collection->size()); 78 | $this->assertThat($collection, CollectionContainsSameElements::with([27, 50])); 79 | } 80 | 81 | /** 82 | * @test 83 | */ 84 | public function removingAbsent(): void 85 | { 86 | /** @var Collection $collection */ 87 | $collection = $this->createInstance(1, 2, 3); 88 | $removingRes = $collection->remove(4); 89 | 90 | $this->assertFalse($removingRes); 91 | $this->assertEquals(3, $collection->size()); 92 | } 93 | 94 | /** 95 | * @test 96 | */ 97 | public function removingFromEmptyCollection(): void 98 | { 99 | $collection = $this->createInstance(); 100 | 101 | $removingRes = $collection->remove(4); 102 | 103 | $this->assertFalse($removingRes); 104 | $this->assertEquals(0, $collection->size()); 105 | } 106 | 107 | /** 108 | * @test 109 | */ 110 | public function containingCheck(): void 111 | { 112 | $collction = $this->createInstance(27, 'string', -11, 50); 113 | $this->assertTrue($collction->contains('string')); 114 | $this->assertTrue($collction->contains(-11)); 115 | $this->assertFalse($collction->contains(11)); 116 | } 117 | 118 | /** 119 | * @test 120 | */ 121 | public function equivalence(): void 122 | { 123 | $collection = $this->createInstance(189, 11, 789); 124 | $anotherCollection = $this->createInstance(189, 11, 789); 125 | $this->assertTrue($collection->equals($anotherCollection)); 126 | $this->assertTrue($anotherCollection->equals($collection)); 127 | } 128 | 129 | /** 130 | * @test 131 | */ 132 | public function sizeDetection(): void 133 | { 134 | $collection = $this->createInstance(27, 'string', -11, 50); 135 | $this->assertEquals(4, $collection->size()); 136 | $collection->remove(-11); 137 | $this->assertEquals(3, $collection->size()); 138 | $collection->add('anotherString'); 139 | $this->assertEquals(4, $collection->size()); 140 | } 141 | 142 | /** 143 | * @test 144 | */ 145 | public function emptiness(): void 146 | { 147 | $collection = $this->createInstance(27, 'string', -11, 50); 148 | $this->assertFalse($collection->isEmpty()); 149 | $collection->clear(); 150 | $this->assertTrue($collection->isEmpty()); 151 | } 152 | 153 | /** 154 | * @test 155 | */ 156 | public function arrayGenerating(): void 157 | { 158 | $collection = $this->createInstance(27, 'string', -11, 50); 159 | $this->assertThat($collection, CollectionContainsSameElements::with([27, 'string', -11, 50])); 160 | } 161 | 162 | /** 163 | * @test 164 | */ 165 | public function coping(): void 166 | { 167 | /** @var Collection $i1 */ 168 | $i1 = $this->createInstance(3, 2, 1); 169 | $i2 = $i1->copy(); 170 | 171 | $this->assertEquals($i1->toArray(), $i2->toArray()); 172 | $this->assertNotSame($i1, $i2); 173 | } 174 | 175 | /** 176 | * @test 177 | */ 178 | public function streaming(): void 179 | { 180 | $i = $this->createInstance(); 181 | 182 | $this->assertInstanceOf(Stream::class, $i->stream()); 183 | } 184 | 185 | /** 186 | * @test 187 | */ 188 | public function addingGroupOffElements(): void 189 | { 190 | /** @var Collection $i */ 191 | $i = $this->createInstance(1, 2, 3); 192 | 193 | $i->addAll([4, 5, 6]); 194 | 195 | $this->assertEquals(6, $i->size()); 196 | $this->assertThat($i, CollectionContainsSameElements::with([1, 2, 3, 4, 5, 6])); 197 | } 198 | 199 | /** 200 | * @test 201 | */ 202 | public function removingWithCollectionAwareInterface(): void 203 | { 204 | $i1 = new TestInteger(1); 205 | $i2 = new TestInteger(2); 206 | $i3 = new TestInteger(3); 207 | 208 | /** @var Collection $collection */ 209 | $collection = $this->createInstance($i1, $i2, $i3); 210 | $res = $collection->remove(new TestInteger(2)); 211 | 212 | $this->assertTrue($res); 213 | $this->assertEquals(2, $collection->size()); 214 | $this->assertFalse($collection->contains($i2)); 215 | } 216 | 217 | /** 218 | * @test 219 | */ 220 | public function removingByAwareCollectionInterfaceWithMixedCollection(): void 221 | { 222 | $i1 = new TestInteger(1); 223 | $i2 = new TestInteger(2); 224 | $i3 = new TestInteger(3); 225 | $i4 = 4; 226 | 227 | /** @var Collection $collection */ 228 | $collection = $this->createInstance($i1, $i2, $i3,$i4); 229 | 230 | $res = $collection->remove(new TestInteger(2)); 231 | 232 | $this->assertTrue($res); 233 | $this->assertEquals(3, $collection->size()); 234 | $this->assertFalse($collection->contains($i2)); 235 | 236 | $this->assertTrue($collection->contains(4)); 237 | 238 | $res = $collection->remove(4); 239 | 240 | $this->assertTrue($res); 241 | $this->assertEquals(2, $collection->size()); 242 | $this->assertFalse($collection->contains(4)); 243 | 244 | $res = $collection->remove(new TestInteger(2)); 245 | 246 | $this->assertFalse($res); 247 | $this->assertEquals(2, $collection->size()); 248 | $this->assertFalse($collection->contains($i2)); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /tests/WS/Utils/Collections/PredicatesFunctionsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(5, $collection->size()); 26 | 27 | $notNullSize = $collection 28 | ->stream() 29 | ->filter(Predicates::notNull()) 30 | ->getCollection() 31 | ->size() 32 | ; 33 | $this->assertEquals(3, $notNullSize); 34 | } 35 | 36 | /** 37 | * @test 38 | */ 39 | public function notResistanceFilter(): void 40 | { 41 | $collection = self::toCollection(null, 1, 2, 3, null); 42 | 43 | $after = $collection 44 | ->stream() 45 | ->filter(Predicates::notResistance()) 46 | ->getCollection() 47 | ; 48 | 49 | $this->assertThat($after, CollectionIsEqual::to($collection)); 50 | } 51 | 52 | /** 53 | * @test 54 | */ 55 | public function lockFilter(): void 56 | { 57 | $collection = self::toCollection(null, 1, 2, 3, null) 58 | ->stream() 59 | ->filter(Predicates::lock()) 60 | ->getCollection() 61 | ; 62 | 63 | $this->assertTrue($collection->isEmpty()); 64 | } 65 | 66 | /** 67 | * @test 68 | */ 69 | public function eachEvenFunctionChecking():void 70 | { 71 | $collection = self::toCollection(1, 2, 3, 4, 5) 72 | ->stream() 73 | ->filter(Predicates::eachEven()) 74 | ->getCollection() 75 | ; 76 | $this->assertThat($collection, CollectionIsEqual::to([2, 4])); 77 | } 78 | 79 | /** 80 | * @test 81 | */ 82 | public function eachNthChecking(): void 83 | { 84 | $collection = self::toCollection(1, 2, 3, 4, 5, 6, 7) 85 | ->stream() 86 | ->filter(Predicates::nth(3)) 87 | ->getCollection() 88 | ; 89 | $this->assertThat($collection, CollectionIsEqual::to([3, 6])); 90 | } 91 | 92 | /** 93 | * @test 94 | */ 95 | public function matchingPropertyFiltering(): void 96 | { 97 | $o1 = (new ExampleObject())->setName('first'); 98 | $o2 = (new ExampleObject())->setName('first'); 99 | $o3 = (new ExampleObject())->setName('second'); 100 | $o4 = (new ExampleObject())->setName('third'); 101 | 102 | $collection = self::toCollection($o1, $o2, $o3, $o4); 103 | 104 | $filtered = $collection 105 | ->stream() 106 | ->filter(Predicates::where('name', 'first')) 107 | ->getCollection() 108 | ; 109 | $this->assertThat($filtered, CollectionIsEqual::to([$o1, $o2])); 110 | 111 | $filtered = $collection 112 | ->stream() 113 | ->filter(Predicates::whereNot('name', 'third')) 114 | ->getCollection() 115 | ; 116 | $this->assertThat($filtered, CollectionIsEqual::to([$o1, $o2, $o3])); 117 | 118 | $filtered = $collection 119 | ->stream() 120 | ->filter(Predicates::whereIn('name', ['second', 'third'])) 121 | ->getCollection() 122 | ; 123 | $this->assertThat($filtered, CollectionIsEqual::to([$o3, $o4])); 124 | 125 | $filtered = $collection 126 | ->stream() 127 | ->filter(Predicates::whereNotIn('name', ['second', 'third'])) 128 | ->getCollection() 129 | ; 130 | $this->assertThat($filtered, CollectionIsEqual::to([$o1, $o2])); 131 | } 132 | 133 | /** 134 | * @test 135 | */ 136 | public function comparingPropertiesFiltering(): void 137 | { 138 | $o1 = (new ExampleObject())->setField(1); 139 | $o2 = (new ExampleObject())->setField(2); 140 | $o3 = (new ExampleObject())->setField(3); 141 | $o4 = (new ExampleObject())->setField(4); 142 | 143 | $collection = self::toCollection($o1, $o2, $o3, $o4); 144 | 145 | $filtered = $collection 146 | ->stream() 147 | ->filter(Predicates::whereGreaterThan('field', 2)) 148 | ->getCollection() 149 | ; 150 | $this->assertThat($filtered, CollectionIsEqual::to([$o3, $o4])); 151 | 152 | $filtered = $collection 153 | ->stream() 154 | ->filter(Predicates::whereLessThan('field', 2)) 155 | ->getCollection() 156 | ; 157 | $this->assertThat($filtered, CollectionIsEqual::to([$o1])); 158 | 159 | $filtered = $collection 160 | ->stream() 161 | ->filter(Predicates::whereGreaterOrEqual('field', 2)) 162 | ->getCollection() 163 | ; 164 | $this->assertThat($filtered, CollectionIsEqual::to([$o2, $o3, $o4])); 165 | 166 | $filtered = $collection 167 | ->stream() 168 | ->filter(Predicates::whereLessOrEqual('field', 2)) 169 | ->getCollection() 170 | ; 171 | $this->assertThat($filtered, CollectionIsEqual::to([$o1, $o2])); 172 | } 173 | 174 | /** 175 | * @test 176 | */ 177 | public function matchingValuesFiltering(): void 178 | { 179 | $collection = self::toCollection(1, 2, 3, 4, 5); 180 | 181 | $filtered = $collection 182 | ->stream() 183 | ->filter(Predicates::equal(2)) 184 | ->getCollection() 185 | ; 186 | $this->assertThat($filtered, CollectionIsEqual::to([2])); 187 | 188 | $filtered = $collection 189 | ->stream() 190 | ->filter(Predicates::not(2)) 191 | ->getCollection() 192 | ; 193 | $this->assertThat($filtered, CollectionIsEqual::to([1, 3, 4, 5])); 194 | 195 | $filtered = $collection 196 | ->stream() 197 | ->filter(Predicates::greaterThan(3)) 198 | ->getCollection() 199 | ; 200 | $this->assertThat($filtered, CollectionIsEqual::to([4, 5])); 201 | 202 | $filtered = $collection 203 | ->stream() 204 | ->filter(Predicates::greaterOrEqual(3)) 205 | ->getCollection() 206 | ; 207 | $this->assertThat($filtered, CollectionIsEqual::to([3, 4, 5])); 208 | 209 | $filtered = $collection 210 | ->stream() 211 | ->filter(Predicates::lessThan(3)) 212 | ->getCollection() 213 | ; 214 | $this->assertThat($filtered, CollectionIsEqual::to([1, 2])); 215 | 216 | $filtered = $collection 217 | ->stream() 218 | ->filter(Predicates::lessOrEqual(3)) 219 | ->getCollection() 220 | ; 221 | $this->assertThat($filtered, CollectionIsEqual::to([1, 2, 3])); 222 | 223 | $filtered = $collection 224 | ->stream() 225 | ->filter(Predicates::in([3, 4, 5, 6])) 226 | ->getCollection() 227 | ; 228 | $this->assertThat($filtered, CollectionIsEqual::to([3, 4, 5])); 229 | 230 | $filtered = $collection 231 | ->stream() 232 | ->filter(Predicates::notIn([3, 4, 5, 6])) 233 | ->getCollection() 234 | ; 235 | $this->assertThat($filtered, CollectionIsEqual::to([1, 2])); 236 | } 237 | 238 | /** 239 | * @test 240 | */ 241 | public function firstValueStreaming(): void 242 | { 243 | // Arrange 244 | $FIRST_VALUE = 5; 245 | $stream = self::toCollection($FIRST_VALUE, 6, 7)->stream(); 246 | 247 | $runningCount = 0; 248 | // Act 249 | $stream 250 | ->filter(Predicates::first()) 251 | ->each(static function ($value) use ($FIRST_VALUE, & $runningCount) { 252 | $runningCount++; 253 | self::assertEquals($FIRST_VALUE, $value); 254 | }); 255 | 256 | // Assert 257 | self::assertEquals(1, $runningCount); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/SerialStream.php: -------------------------------------------------------------------------------- 1 | list = $collection->copy(); 21 | } else { 22 | $this->list = $this->emptyList(); 23 | $this->list->addAll($collection->toArray()); 24 | } 25 | } 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public function each(callable $consumer): Stream 31 | { 32 | $i = 0; 33 | foreach ($this->list as $item) { 34 | $consumer($item, $i++); 35 | } 36 | return $this; 37 | } 38 | 39 | /** 40 | * @inheritDoc 41 | */ 42 | public function filter(callable $predicate): Stream 43 | { 44 | $collection = $this->list; 45 | $this->list = $this->emptyList(); 46 | 47 | foreach ($collection as $item) { 48 | if ($predicate($item)) { 49 | $this->list->add($item); 50 | } 51 | } 52 | 53 | return $this; 54 | } 55 | 56 | public function reorganize(callable $reorganizer): Stream 57 | { 58 | $reorganizedCollection = $reorganizer($this->list->copy()); 59 | if (! $reorganizedCollection instanceof Collection) { 60 | throw new RuntimeException('Result set of reorganizer call must be instance of Collection interface'); 61 | } 62 | $this->list = $reorganizedCollection; 63 | 64 | return $this; 65 | } 66 | 67 | /** 68 | * @inheritDoc 69 | */ 70 | public function allMatch(callable $predicate): bool 71 | { 72 | foreach ($this->list as $item) { 73 | if (!$predicate($item)) { 74 | return false; 75 | } 76 | } 77 | return true; 78 | } 79 | 80 | /** 81 | * @inheritDoc 82 | */ 83 | public function anyMatch(callable $predicate): bool 84 | { 85 | foreach ($this->list as $item) { 86 | if ($predicate($item)) { 87 | return true; 88 | } 89 | } 90 | 91 | return false; 92 | } 93 | 94 | /** 95 | * @inheritDoc 96 | */ 97 | public function map(callable $converter): Stream 98 | { 99 | $collection = $this->list; 100 | $this->list = $this->emptyList(); 101 | 102 | foreach ($collection as $item) { 103 | $this->list->add($converter($item)); 104 | } 105 | return $this; 106 | } 107 | 108 | /** 109 | * @inheritDoc 110 | */ 111 | public function sort(callable $comparator): Stream 112 | { 113 | $collection = $this->getCollection(); 114 | $this->list = $this->emptyList(); 115 | 116 | $array = $collection->toArray(); 117 | usort($array, $comparator); 118 | foreach ($array as $item) { 119 | $this->list->add($item); 120 | } 121 | 122 | return $this; 123 | } 124 | 125 | public function sortBy(callable $extractor): Stream 126 | { 127 | $values = []; 128 | $map = []; 129 | $this->each(static function ($el) use ($extractor, & $map, & $values) { 130 | $value = $extractor($el); 131 | if (!is_scalar($value)) { 132 | throw new RuntimeException('Only scalar value can be as result of sort extractor'); 133 | } 134 | $values[] = $value; 135 | $map[$value.''][] = $el; 136 | }); 137 | sort($values); 138 | $newList = $this->emptyList(); 139 | foreach ($values as $value) { 140 | $key = $value.''; 141 | $els = $map[$key] ?? []; 142 | $newList->addAll($els); 143 | unset($map[$key]); 144 | } 145 | $this->list = $newList; 146 | 147 | return $this; 148 | } 149 | 150 | public function sortByDesc(callable $extractor): Stream 151 | { 152 | $this->sortBy($extractor) 153 | ->reverse(); 154 | 155 | return $this; 156 | } 157 | 158 | /** 159 | * @inheritDoc 160 | */ 161 | public function sortDesc(callable $comparator): Stream 162 | { 163 | $this->sort($comparator) 164 | ->reverse(); 165 | 166 | return $this; 167 | } 168 | 169 | public function reverse(): Stream 170 | { 171 | $array = $this->list->toArray(); 172 | $reversedArray = array_reverse($array); 173 | $this->list = $this->emptyList(); 174 | $this->list->addAll($reversedArray); 175 | return $this; 176 | } 177 | 178 | /** 179 | * @inheritDoc 180 | */ 181 | public function collect(callable $collector) 182 | { 183 | return $collector($this->getCollection()->copy()); 184 | } 185 | 186 | /** 187 | * @inheritDoc 188 | */ 189 | public function findAny() 190 | { 191 | $size = $this->list->size(); 192 | if ($size === 0) { 193 | return null; 194 | } 195 | /** @noinspection PhpUnhandledExceptionInspection */ 196 | $rIndex = random_int(0, $size - 1); 197 | $pointer = 0; 198 | $item = null; 199 | foreach ($this->list as $item) { 200 | if ($rIndex === $pointer++) { 201 | break; 202 | } 203 | } 204 | return $item; 205 | } 206 | 207 | /** 208 | * @inheritDoc 209 | */ 210 | public function findFirst(callable $filter = null) 211 | { 212 | if (!$filter) { 213 | /** @noinspection LoopWhichDoesNotLoopInspection */ 214 | foreach ($this->list as $item) { 215 | return $item; 216 | } 217 | return null; 218 | } 219 | /** @noinspection LoopWhichDoesNotLoopInspection */ 220 | foreach ($this->list as $item) { 221 | if ($filter($item)) { 222 | return $item; 223 | } 224 | } 225 | return null; 226 | } 227 | 228 | /** 229 | * @inheritDoc 230 | */ 231 | public function min(callable $comparator) 232 | { 233 | $collection = $this->getCollection(); 234 | if ($collection->size() === 0) { 235 | return null; 236 | } 237 | 238 | $array = $collection->toArray(); 239 | 240 | $el = array_shift($array); 241 | 242 | foreach ($array as $item) { 243 | if ($comparator($item, $el) < 0) { 244 | $el = $item; 245 | } 246 | } 247 | 248 | return $el; 249 | } 250 | 251 | /** 252 | * @inheritDoc 253 | */ 254 | public function max(callable $comparator) 255 | { 256 | $collection = $this->getCollection(); 257 | if ($collection->size() === 0) { 258 | return null; 259 | } 260 | 261 | $array = $collection->toArray(); 262 | $el = null; 263 | 264 | foreach ($array as $item) { 265 | if ($comparator($item, $el) > 0) { 266 | $el = $item; 267 | } 268 | } 269 | 270 | return $el; 271 | } 272 | 273 | /** 274 | * @inheritDoc 275 | */ 276 | public function reduce(callable $accumulator, $initialValue = null) 277 | { 278 | $accumulate = $initialValue; 279 | foreach ($this->list as $item) { 280 | $accumulate = $accumulator($item, $accumulate); 281 | } 282 | return $accumulate; 283 | } 284 | 285 | public function getCollection(): Collection 286 | { 287 | return $this->list->copy(); 288 | } 289 | 290 | private function emptyList(): Collection 291 | { 292 | return ArrayList::of(); 293 | } 294 | 295 | public function findLast() 296 | { 297 | $array = $this->list->toArray(); 298 | return array_pop($array); 299 | } 300 | 301 | public function walk(callable $consumer, ?int $limit = null): Stream 302 | { 303 | $iterationsCount = $limit ?? $this->list->size(); 304 | foreach ($this->list as $i => $item) { 305 | $consumerRes = $consumer($item, $i); 306 | if ($consumerRes === false) { 307 | break; 308 | } 309 | if ($i + 1 >= $iterationsCount) { 310 | break; 311 | } 312 | } 313 | 314 | return $this; 315 | } 316 | 317 | public function limit(int $size): Stream 318 | { 319 | $newCollection = $this->emptyList(); 320 | $this->walk(static function ($el) use ($newCollection) { 321 | $newCollection->add($el); 322 | }, $size); 323 | 324 | $this->list = $newCollection; 325 | return $this; 326 | } 327 | 328 | public function when(bool $condition): Stream 329 | { 330 | if (!$condition) { 331 | return new DummyStreamDecorator($this); 332 | } 333 | 334 | return $this; 335 | } 336 | 337 | public function always(): Stream 338 | { 339 | return $this; 340 | } 341 | 342 | /** 343 | * @inheritDoc 344 | */ 345 | public function toArray(): array 346 | { 347 | return $this 348 | ->getCollection() 349 | ->toArray() 350 | ; 351 | } 352 | 353 | /** 354 | * @inheritDoc 355 | */ 356 | public function getSet(): Set 357 | { 358 | return new HashSet($this->toArray()); 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /src/WS/Utils/Collections/Functions/Predicates.php: -------------------------------------------------------------------------------- 1 | blocked all tries 16 | * @return Closure 17 | */ 18 | public static function lock(): Closure 19 | { 20 | return static function (): bool { 21 | return false; 22 | }; 23 | } 24 | 25 | /** 26 | * Returns passed all not null elements 27 | * @return Closure 28 | */ 29 | public static function notNull(): Closure 30 | { 31 | return static function ($el): bool { 32 | return $el !== null; 33 | }; 34 | } 35 | 36 | /** 37 | * Returns passed all tries 38 | * @return Closure 39 | */ 40 | public static function notResistance(): Closure 41 | { 42 | return static function (): bool { 43 | return true; 44 | }; 45 | } 46 | 47 | /** 48 | * Returns passed each even element of call 49 | * @return Closure 50 | */ 51 | public static function eachEven(): Closure 52 | { 53 | $isEven = false; 54 | return static function () use (& $isEven) { 55 | $res = $isEven; 56 | $isEven = !$isEven; 57 | 58 | return $res; 59 | }; 60 | } 61 | 62 | /** 63 | * Returns passed each nth element of call 64 | * @param $number 65 | * @return Closure 66 | */ 67 | public static function nth($number): Closure 68 | { 69 | $counter = 0; 70 | return static function () use ($number, & $counter) { 71 | $res = ++$counter % $number === 0; 72 | if ($res) { 73 | $counter = 0; 74 | } 75 | return $res; 76 | }; 77 | } 78 | 79 | /** 80 | * Returns passed with equal value 81 | * @param $value 82 | * @return Closure 83 | */ 84 | public static function equal($value): Closure 85 | { 86 | return static function ($el) use ($value): bool { 87 | return $el === $value; 88 | }; 89 | } 90 | 91 | /** 92 | * Returns passed unique element at once 93 | */ 94 | public static function lockDuplicated(?callable $caster = null): Closure 95 | { 96 | $set = new HashSet(); 97 | return static function ($el) use ($set, $caster): bool { 98 | $el = isset($caster) ? $caster($el) : $el; 99 | $res = !$set->contains($el); 100 | $set->add($el); 101 | return $res; 102 | }; 103 | } 104 | 105 | /** 106 | * Returns passed with less than value comparing 107 | * @param $value 108 | * @return Closure 109 | */ 110 | public static function lessThan($value): Closure 111 | { 112 | return static function ($el) use ($value): bool { 113 | return $el < $value; 114 | }; 115 | } 116 | 117 | /** 118 | * Returns passed with less or equal value comparing 119 | * @param $value 120 | * @return Closure 121 | */ 122 | public static function lessOrEqual($value): Closure 123 | { 124 | return static function ($el) use ($value): bool { 125 | return $el <= $value; 126 | }; 127 | } 128 | 129 | /** 130 | * Returns passed with more than value comparing 131 | * @param $value 132 | * @return Closure 133 | */ 134 | public static function greaterThan($value): Closure 135 | { 136 | return static function ($el) use ($value): bool { 137 | return $el > $value; 138 | }; 139 | } 140 | 141 | /** 142 | * Returns passed with more or equal value comparing 143 | * @param $value 144 | * @return Closure 145 | */ 146 | public static function greaterOrEqual($value): Closure 147 | { 148 | return static function ($el) use ($value): bool { 149 | return $el >= $value; 150 | }; 151 | } 152 | 153 | /** 154 | * Returns passed with not value comparing 155 | * @param $value 156 | * @return Closure 157 | */ 158 | public static function not($value): Closure 159 | { 160 | return static function ($el) use ($value): bool { 161 | return $el !== $value; 162 | }; 163 | } 164 | 165 | /** 166 | * Returns passed with include of set value comparing 167 | * @param array $values 168 | * @return Closure 169 | */ 170 | public static function in(array $values): Closure 171 | { 172 | return static function ($el) use ($values): bool { 173 | return in_array($el, $values, true); 174 | }; 175 | } 176 | 177 | /** 178 | * Returns passed with not include of set value comparing 179 | * @param array $values 180 | * @return Closure 181 | */ 182 | public static function notIn(array $values): Closure 183 | { 184 | return static function ($el) use ($values): bool { 185 | return !in_array($el, $values, true); 186 | }; 187 | } 188 | 189 | /** 190 | * Returns passed with where object property value comparing 191 | * @param string $property 192 | * @param $value 193 | * @return Closure 194 | */ 195 | public static function where(string $property, $value): Closure 196 | { 197 | return static function ($ob) use ($property, $value) { 198 | return $value === ObjectFunctions::getPropertyValue($ob, $property); 199 | }; 200 | } 201 | 202 | /** 203 | * Returns passed with where not object property value comparing 204 | * @param string $property 205 | * @param $value 206 | * @return Closure 207 | */ 208 | public static function whereNot(string $property, $value): Closure 209 | { 210 | return static function ($ob) use ($property, $value) { 211 | return $value !== ObjectFunctions::getPropertyValue($ob, $property); 212 | }; 213 | } 214 | 215 | /** 216 | * Returns passed with where present in a set of object property value comparing 217 | * @param string $property 218 | * @param array $values 219 | * @return Closure 220 | */ 221 | public static function whereIn(string $property, array $values): Closure 222 | { 223 | return static function ($ob) use ($property, $values) { 224 | return in_array(ObjectFunctions::getPropertyValue($ob, $property), $values, true); 225 | }; 226 | } 227 | 228 | /** 229 | * Returns passed with where not present in a set of object property value comparing 230 | * @param string $property 231 | * @param array $values 232 | * @return Closure 233 | */ 234 | public static function whereNotIn(string $property, array $values): Closure 235 | { 236 | return static function ($ob) use ($property, $values) { 237 | return !in_array(ObjectFunctions::getPropertyValue($ob, $property), $values, true); 238 | }; 239 | } 240 | 241 | /** 242 | * Returns passed with more than object property value comparing 243 | * @param string $property 244 | * @param $value 245 | * @return Closure 246 | */ 247 | public static function whereGreaterThan(string $property, $value): Closure 248 | { 249 | return static function ($ob) use ($property, $value) { 250 | return ObjectFunctions::getPropertyValue($ob, $property) > $value; 251 | }; 252 | } 253 | 254 | /** 255 | * Returns passed with less than object property value comparing 256 | * @param string $property 257 | * @param $value 258 | * @return Closure 259 | */ 260 | public static function whereLessThan(string $property, $value): Closure 261 | { 262 | return static function ($ob) use ($property, $value) { 263 | return ObjectFunctions::getPropertyValue($ob, $property) < $value; 264 | }; 265 | } 266 | 267 | /** 268 | * Returns passed with more or equal object property value comparing 269 | * @param string $property 270 | * @param $value 271 | * @return Closure 272 | */ 273 | public static function whereGreaterOrEqual(string $property, $value): Closure 274 | { 275 | return static function ($ob) use ($property, $value) { 276 | return ObjectFunctions::getPropertyValue($ob, $property) >= $value; 277 | }; 278 | } 279 | 280 | /** 281 | * Returns passed with less or equal object property value comparing 282 | * @param string $property 283 | * @param $value 284 | * @return Closure 285 | */ 286 | public static function whereLessOrEqual(string $property, $value): Closure 287 | { 288 | return static function ($ob) use ($property, $value) { 289 | return ObjectFunctions::getPropertyValue($ob, $property) <= $value; 290 | }; 291 | } 292 | 293 | /** 294 | * Returns passed only one time with first element 295 | * @return Closure 296 | */ 297 | public static function first(): Closure 298 | { 299 | $runOut = false; 300 | return static function () use (& $runOut) { 301 | if ($runOut) { 302 | return false; 303 | } 304 | $runOut = true; 305 | return true; 306 | }; 307 | } 308 | } 309 | --------------------------------------------------------------------------------