diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ff8f13..0719b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- Added all `_(u)diff_`, `_(u)assoc` and `_(u)intersect` methods as iterator functions. ## [1.0.1] - 2021-08-29 ### Changed diff --git a/composer.json b/composer.json index f9a46bd..bd9a922 100644 --- a/composer.json +++ b/composer.json @@ -6,8 +6,9 @@ "php": "^7.4|^8.0" }, "require-dev": { - "pestphp/pest": "^1.15", - "vimeo/psalm": "^4.9" + "pestphp/pest": "^1.17", + "vimeo/psalm": "^4.9", + "phpbench/phpbench": "^1.1" }, "autoload": { "psr-4": { @@ -17,11 +18,20 @@ "src/iterator_functions.php" ] }, + "autoload-dev": { + "psr-4": { + "DoekeNorg\\IteratorFunctions\\Tests\\": "tests" + } + }, "license": "MIT", "authors": [ { "name": "Doeke Norg", "email": "doekenorg@gmail.com" } - ] + ], + "scripts": { + "test": "./vendor/bin/pest", + "fix": "php-cs-fixer fix --config=.php_cs.dist.php --allow-risky=yes" + } } diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..fed2503 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,4 @@ +{ + "$schema":"./vendor/phpbench/phpbench/phpbench.schema.json", + "runner.bootstrap": "vendor/autoload.php" +} diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php new file mode 100644 index 0000000..673bd37 --- /dev/null +++ b/src/Iterator/DiffIterator.php @@ -0,0 +1,165 @@ +iterator_compare = new \AppendIterator(); + $this->value_compare = self::defaultCompare(); + + foreach ($iterators as $iterator_compare) { + $this->iterator_compare->append($iterator_compare); + } + } + + /** + * Extracts the params from a function call. + * @param array $params The provided arguments. + * @return mixed The params. + */ + final public static function extractParams(array $params): array + { + $result = ['iterator' => null, 'iterators' => [], 'callbacks' => []]; + + $iterator = array_shift($params); + if (!$iterator instanceof \Iterator) { + throw new \InvalidArgumentException('First parameter must be an iterator.'); + } + + $result['iterator'] = $iterator; + + while (($argument = array_shift($params))) { + if (!$argument instanceof \Iterator && !is_callable($argument)) { + throw new \InvalidArgumentException(sprintf( + 'Argument should be an iterator or callback; "%s" given.', + is_string($argument) ? $argument : get_class($argument), + )); + } + $type = $argument instanceof \Iterator + ? 'iterators' + : 'callbacks'; + + if ($type === 'iterators' && count($result['callbacks']) !== 0) { + throw new \InvalidArgumentException('An iterator may not be provided after a callback.'); + } + $result[$type][] = $argument; + } + + if (count($result['iterators']) === 0) { + throw new \InvalidArgumentException('There is no iterator to match against.'); + } + + return array_values($result); + } + + /** + * @inheritdoc + */ + public function accept(): bool + { + if ($this->key_compare && $this->assoc_compare) { + throw new \InvalidArgumentException('Can only use one of "withKey" or "withAssociative", not both.'); + } + + foreach ($this->iterator_compare as $key => $value) { + if ($this->key_compare && ($this->key_compare)($this->key(), $key) === 0) { + return $this->equal_accept; + } + + if (($this->value_compare)($this->current(), $value) === 0) { + if ($this->assoc_compare && (($this->assoc_compare)($this->key(), $key) !== 0)) { + continue; + } + + return $this->equal_accept; + } + } + + return !$this->equal_accept; + } + + /** + * Sets the iterator whether to compare with an extra key check. + * @param callable|null $callback Optional callback to use for comparison. + * @return $this The iterator. + */ + public function withAssociative(?callable $callback = null): self + { + $this->assoc_compare = $callback ?? self::defaultCompare(); + + return $this; + } + + /** + * Sets the iterator to compare against the key. + * @param null|callable $callback Optional callable to perform as key compare function. + * @return $this The iterator. + */ + public function withKey(?callable $callback = null): self + { + $this->key_compare = $callback ?? self::defaultCompare(); + + return $this; + } + + /** + * Sets the iterator to compare the value by callback. + * @param callable $callback Callable to perform as compare function. + * @return $this The iterator. + */ + public function withCallback(callable $callback): self + { + $this->value_compare = $callback; + + return $this; + } + + /** + * The default function to use for comparing. + * @return callable(mixed $current, mixed $compare):int 0 when the same, -1 of 1 when different. + */ + final protected static function defaultCompare(): callable + { + return static fn ($current, $compare) => $current <=> $compare; + } +} diff --git a/src/Iterator/IntersectIterator.php b/src/Iterator/IntersectIterator.php new file mode 100644 index 0000000..5f07dd6 --- /dev/null +++ b/src/Iterator/IntersectIterator.php @@ -0,0 +1,14 @@ +getMessage(), (int) $e->getCode(), $e); + return; // Will not happen. } + // @codeCoverageIgnoreEnd if (!$function->isVariadic() && $function->getNumberOfParameters() !== count($iterators)) { throw new \InvalidArgumentException('The callback needs as many arguments as provided iterators.'); diff --git a/src/iterator_functions.php b/src/iterator_functions.php index d862ebe..ebd473c 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -1,7 +1,9 @@ withAssociative(); + } +} + +if (!function_exists('iterator_diff_key')) { + /** + * Computes the difference of iterators by key check. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @return DiffIterator An iterator with the difference. + */ + function iterator_diff_key(\Iterator $iterator, \Iterator ...$iterators): DiffIterator + { + return (new DiffIterator($iterator, ...$iterators))->withKey(); + } +} + +if (!function_exists('iterator_diff_uassoc')) { + /** + * Computes the difference of iterators with extra by key check using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable $callback The callback that computes the difference on the keys. Must return an `int`. + * @return DiffIterator An iterator with the difference. + */ + function iterator_diff_uassoc(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No associative callback provided.'); + } + + return (new DiffIterator($iterator, ...$iterators))->withAssociative(...$callbacks); + } +} + +if (!function_exists('iterator_diff_ukey')) { + /** + * Computes the difference of iterators by key check using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable $callback The callback that computes the difference on the keys. Must return an `int`. + * @return DiffIterator An iterator with the difference. + */ + function iterator_diff_ukey(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No key callback provided.'); + } + + return (new DiffIterator($iterator, ...$iterators))->withKey(...$callbacks); + } +} + +if (!function_exists('iterator_udiff')) { + /** + * Computes the difference of iterators using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current_value, mixed $compare_value):int The callback to perform. + * @return DiffIterator An iterator with the difference. + */ + function iterator_udiff(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No diff callback provided.'); + } + + return (new DiffIterator($iterator, ...$iterators))->withCallback(...$callbacks); + } +} + +if (!function_exists('iterator_udiff_assoc')) { + /** + * Computes the difference of iterators using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current_value, mixed $compare_value):int The callback to perform. + * @return DiffIterator An iterator with the difference. + */ + function iterator_udiff_assoc(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No diff callback provided.'); + } + + return (new DiffIterator($iterator, ...$iterators)) + ->withCallback(...$callbacks) + ->withAssociative(); + } +} + +if (!function_exists('iterator_udiff_uassoc')) { + /** + * Computes the difference of iterators using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current_value, mixed $compare_value):int The callback to perform. + * @param callable(mixed $current_key, mixed $compare_key):int The callback to perform. + * @return DiffIterator An iterator with the difference. + */ + function iterator_udiff_uassoc(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No diff callback provided.'); + } + + if (count($callbacks) === 1) { + throw new \InvalidArgumentException('No associative callback provided.'); + } + + return (new DiffIterator($iterator, ...$iterators)) + ->withCallback($callbacks[0]) + ->withAssociative($callbacks[1]); + } +} + if (!function_exists('iterator_filter')) { /** * Filters elements off an iterator using a callback function. @@ -31,7 +176,7 @@ function iterator_column(\Traversable $iterator, $column_key, $index_key = null) */ function iterator_filter(Iterator $iterator, ?callable $callback = null): \CallbackFilterIterator { - return new \CallbackFilterIterator($iterator, $callback ?? static fn ($value) => !empty($value)); + return new \CallbackFilterIterator($iterator, $callback ?? static fn ($value): bool => !empty($value)); } } @@ -47,6 +192,149 @@ function iterator_flip(Iterator $iterator): FlipIterator } } +if (!function_exists('iterator_intersect')) { + /** + * Computes the difference between iterators. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @return IntersectIterator An iterator with the intersection. + */ + function iterator_intersect(\Iterator $iterator, \Iterator ...$iterators): IntersectIterator + { + return new IntersectIterator($iterator, ...$iterators); + } +} + +if (!function_exists('iterator_intersect_assoc')) { + /** + * Computes the difference between iterators with extra key check. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @return IntersectIterator An iterator with the intersection. + */ + function iterator_intersect_assoc(\Iterator $iterator, \Iterator ...$iterators): IntersectIterator + { + return (new IntersectIterator($iterator, ...$iterators))->withAssociative(); + } +} +if (!function_exists('iterator_intersect_key')) { + /** + * Computes the intersection of iterators by key check. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @return DiffIterator An iterator with the difference. + */ + function iterator_intersect_key(\Iterator $iterator, \Iterator ...$iterators): DiffIterator + { + return (new IntersectIterator($iterator, ...$iterators))->withKey(); + } +} + +if (!function_exists('iterator_intersect_uassoc')) { + /** + * Computes the difference between iterators with extra key check. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current, mixed $compare):int $callback The callback to use for comparing. + * @return IntersectIterator An iterator with the intersection. + */ + function iterator_intersect_uassoc(): IntersectIterator + { + [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No associative callback provided.'); + } + + return (new IntersectIterator($iterator, ...$iterators))->withAssociative(...$callbacks); + } +} + +if (!function_exists('iterator_intersect_ukey')) { + /** + * Computes the intersection of iterators by key check using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current, mixed $compare):int $callback The callback to use for comparing. + * @return IntersectIterator An iterator with the intersection. + */ + function iterator_intersect_ukey(): IntersectIterator + { + [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No key callback provided.'); + } + + return (new IntersectIterator($iterator, ...$iterators))->withKey(...$callbacks); + } +} + +if (!function_exists('iterator_uintersect')) { + /** + * Computes the intersection of iterators using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current_value, mixed $compare_value):int The callback to perform. + * @return IntersectIterator An iterator with the intersection. + */ + function iterator_uintersect(): IntersectIterator + { + [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No intersect callback provided.'); + } + + return (new IntersectIterator($iterator, ...$iterators))->withCallback(...$callbacks); + } +} + +if (!function_exists('iterator_uintersect_assoc')) { + /** + * Computes the intersection of iterators using a callback. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current_value, mixed $compare_value):int The callback to perform. + * @return IntersectIterator An iterator with the intersection. + */ + function iterator_uintersect_assoc(): IntersectIterator + { + [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No intersect callback provided.'); + } + + return (new IntersectIterator($iterator, ...$iterators)) + ->withCallback(...$callbacks) + ->withAssociative(); + } +} + +if (!function_exists('iterator_uintersect_uassoc')) { + /** + * Computes the intersection of iterators using a callback on the value and the key. + * @param \Iterator $iterator The iterator to compare from. + * @param \Iterator ...$iterators The iterators to compare against. + * @param callable(mixed $current_value, mixed $compare_value):int The callable to perform on the value. + * @param callable(mixed $current_key, mixed $compare_key):int The callback to perform on the key. + * @return IntersectIterator An iterator with the difference. + */ + function iterator_uintersect_uassoc(): IntersectIterator + { + [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); + + if (count($callbacks) === 0) { + throw new \InvalidArgumentException('No intersect callback provided.'); + } + + if (count($callbacks) === 1) { + throw new \InvalidArgumentException('No associative callback provided.'); + } + + return (new IntersectIterator($iterator, ...$iterators)) + ->withCallback($callbacks[0]) + ->withAssociative($callbacks[1]); + } +} + if (!function_exists('iterator_keys')) { /** * Returns an iterator that produces only the keys of the inner iterator. diff --git a/tests/Assets/Functions.php b/tests/Assets/Functions.php new file mode 100644 index 0000000..ffb175f --- /dev/null +++ b/tests/Assets/Functions.php @@ -0,0 +1,11 @@ +id = $id; + $this->name = $name; + + // Make this model a bit heavier by creating some data + $this->data = range(1, 100); + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/tests/Benchmark/IteratorDiffBench.php b/tests/Benchmark/IteratorDiffBench.php new file mode 100644 index 0000000..8919d58 --- /dev/null +++ b/tests/Benchmark/IteratorDiffBench.php @@ -0,0 +1,36 @@ + $i, 'name' => 'name ' . $i]; + } + + return $data; + } + + /** + * @Revs(100) + * @Iterations(5) + */ + public function benchArrayMap() + { + $data = $this->getData(); + $iterator = array_map([$this, 'map'], $data); + foreach ($iterator as $model) { + $model->getName(); + } + } + + /** + * @Revs(100) + * @Iterations(5) + */ + public function benchIteratorMap() + { + $data = $this->getData(); + $iterator = iterator_map([$this, 'map'], $data); + foreach ($iterator as $model) { + $model->getName(); + } + } + + public function map(array $user): Model + { + return new Model($user['id'], $user['name']); + } +} diff --git a/tests/Functions/IteratorColumnTest.php b/tests/Functions/IteratorColumnTest.php index d12d031..a21ee07 100644 --- a/tests/Functions/IteratorColumnTest.php +++ b/tests/Functions/IteratorColumnTest.php @@ -17,6 +17,7 @@ expect(iterator_to_array($column_iterator))->toBe(['Title 1', 'Title 2']); }); + it('returns a ColumnIterator', function () { $iterator = new ArrayIterator(); $column_iterator = iterator_column($iterator, 'title'); diff --git a/tests/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php new file mode 100644 index 0000000..84d0400 --- /dev/null +++ b/tests/Functions/IteratorDiffTest.php @@ -0,0 +1,164 @@ +toBe([0 => 1]); + expect(iterator_to_array($result_2))->toBe([2 => 4]); +}); + +it('diffs more than two iterators', function () { + $iterator_1 = new ArrayIterator([1, 2, 3, 'one', 'two', 'three']); + $iterator_2 = new ArrayIterator([2, 3, 4]); + $iterator_3 = new ArrayIterator(['three', 'four', 'five']); + + $result = iterator_diff($iterator_1, $iterator_2, $iterator_3); + + expect(iterator_to_array($result))->toBe([0 => 1, 3 => 'one', 4 => 'two']); +}); + +/** + * Tests for {@see iterator_diff_assoc()}. + */ +it('diffs two iterators associatively', function () { + $iterator_1 = new ArrayIterator(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); + $iterator_2 = new ArrayIterator(['a' => 'green', 'yellow', 'red']); + + $result = iterator_diff_assoc($iterator_1, $iterator_2); + + expect(iterator_to_array($result))->toBe(['b' => 'brown', 'c' => 'blue', 0 => 'red']); +}); + +/** + * Tests for {@see iterator_diff_key()}. + */ + +it('can diff by key', function () { + $iterator_1 = new ArrayIterator(['blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4]); + $iterator_2 = new ArrayIterator(['green' => 5, 'yellow' => 7, 'cyan' => 8]); + + $result = iterator_diff_key($iterator_1, $iterator_2); + expect(iterator_to_array($result))->toBe(['blue' => 1, 'red' => 2, 'purple' => 4]); +}); + +/** + * Tests for {@see iterator_diff_ukey()}. + */ +it('can diff by key using a callback', function () { + $compare_func = function ($key_1, $key_2) { + // green is always different + if ($key_1 === $key_2 && !in_array('green', [$key_1, $key_2], true)) { + return 0; + } + + return 1; + }; + $iterator_1 = new ArrayIterator(['blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4]); + $iterator_2 = new ArrayIterator(['green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan' => 8]); + + $result = iterator_diff_ukey($iterator_1, $iterator_2, $compare_func); + expect(iterator_to_array($result))->toBe(['red' => 2, 'green' => 3, 'purple' => 4]); +}); + +it('can diff with key validation using a callback', function () { + $compare_func = function ($key_1, $key_2) { + if ($key_1 === 0 && $key_2 === 1) { + // Red is on 0 in first iterator, and on 1 in second. + return 0; // Let's consider them the same. + } + + return $key_1 <=> $key_2; + }; + + $iterator_1 = new ArrayIterator(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); + $iterator_2 = new ArrayIterator(['a' => 'green', 'yellow', 'red']); + + $result = iterator_diff_uassoc($iterator_1, $iterator_2, $compare_func); + + expect(iterator_to_array($result))->toBe(['b' => 'brown', 'c' => 'blue']); +}); + +/** + * Tests for {@see iterator_udiff()}. + */ +it('can diff using a callback', function () { + $compare_func = function ($value_1, $value_2) { + // 2 is always different. + if ($value_1 === $value_2 && !in_array(2, [$value_1, $value_2], true)) { + return 0; + } + + return 1; + }; + $iterator_1 = new ArrayIterator([1, 2, 3]); + $iterator_2 = new ArrayIterator([2, 3, 4]); + + $result = iterator_udiff($iterator_1, $iterator_2, $compare_func); + expect(iterator_to_array($result))->toBe([1, 2]); +}); + +/** + * Test for {@see iterator_udiff_assoc()}. + */ +it('can diff using a callback and a key validation', function () { + $compare_func = function ($value_1, $value_2) { + if ($value_1 === 'apple' && $value_2 === 'orange') { + return 0; // nothing to compare, same fruit + } + + return $value_1 <=> $value_2; + }; + + $iterator_1 = new ArrayIterator(['apple', 'pear', 'lime']); + $iterator_2 = new ArrayIterator(['orange', 'berry']); + + $result = iterator_udiff_assoc($iterator_1, $iterator_2, $compare_func); + expect(iterator_to_array($result))->toBe([1 => 'pear', 2 => 'lime']); +}); + +/** + * Test for {@see iterator_udiff_uassoc()}. + */ +it('can diff using a callback and a key validation using a callback', function () { + $value_compare = function ($value_1, $value_2) { + if ($value_1 === 'apple' && $value_2 === 'orange') { + return 0; // nothing to compare, same fruit + } + + return $value_1 <=> $value_2; + }; + + $key_compare = function ($value_1, $value_2) { + if ($value_1 === 0 && $value_2 === 1) { + return 0; // nothing to compare, same fruit + } + + return $value_1 <=> $value_2; + }; + + $iterator_1 = new ArrayIterator(['apple', 'pear', 'lime']); + $iterator_2 = new ArrayIterator(['berry', 'orange']); + + $result = iterator_udiff_uassoc($iterator_1, $iterator_2, $value_compare, $key_compare); + expect(iterator_to_array($result))->toBe([1 => 'pear', 2 => 'lime']); +}); + +test('exceptions are thrown', function (string $function, string $expected_message, array $args) { + expect(static fn () => $function(...$args))->toThrow($expected_message); +}) + ->with([ + ['iterator_diff_uassoc', 'No associative callback provided.', [$it = new ArrayIterator(), $it]], + ['iterator_diff_ukey', 'No key callback provided.', [$it, $it]], + ['iterator_udiff', 'No diff callback provided.', [$it, $it]], + ['iterator_udiff_assoc', 'No diff callback provided.', [$it, $it]], + ['iterator_udiff_uassoc', 'No diff callback provided.', [$it, $it]], + ['iterator_udiff_uassoc', 'No associative callback provided.', [$it, $it, $callback = fn () => '']], + ]); diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php new file mode 100644 index 0000000..918f8e3 --- /dev/null +++ b/tests/Functions/IteratorIntersectTest.php @@ -0,0 +1,181 @@ +toBe([1 => 2, 2 => 3]); + expect(iterator_to_array($iterator_intersect_2))->toBe([0 => 2, 1 => 3]); +}); + +it('compares via string cast', function () { + $iterator_1 = new ArrayIterator([1, '02', 3]); + $iterator_2 = new ArrayIterator([2, 3, 4]); + + $iterator_intersect = iterator_intersect($iterator_1, $iterator_2); + $iterator_intersect_2 = iterator_intersect($iterator_2, $iterator_1); + + expect(iterator_to_array($iterator_intersect))->toBe([1 => '02', 2 => 3]); + expect(iterator_to_array($iterator_intersect_2))->toBe([0 => 2, 1 => 3]); +}); + +it('intersects more than two iterators', function () { + $iterator_1 = new ArrayIterator([1, 2, 3, 'one', 'two', 'three']); + $iterator_2 = new ArrayIterator([2, 3, 4]); + $iterator_3 = new ArrayIterator(['three', 'four', 'five']); + + $iterator_intersect = iterator_intersect($iterator_1, $iterator_2, $iterator_3); + + expect(iterator_to_array($iterator_intersect))->toBe([1 => 2, 2 => 3, 5 => 'three']); +}); + +/** + * Tests for {@see iterator_intersect_assoc()}. + */ + +it('intersects two iterators associatively', function () { + $iterator_1 = new ArrayIterator(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); + $iterator_2 = new ArrayIterator(['a' => 'green', 'yellow', 'red']); + + $result = iterator_intersect_assoc($iterator_1, $iterator_2); + + expect(iterator_to_array($result))->toBe(['a' => 'green']); +}); + +/** + * Tests for {@see iterator_intersect_key()}. + */ + +it('can intersect by key', function () { + $iterator_1 = new ArrayIterator(['blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4]); + $iterator_2 = new ArrayIterator(['green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan' => 8]); + + $result = iterator_intersect_key($iterator_1, $iterator_2); + expect(iterator_to_array($result))->toBe(['blue' => 1, 'green' => 3]); +}); + +/** + * Tests for {@see iterator_intersect_uassoc()}. + */ + +it('intersects two iterators associatively with callback', function () { + $compare_func = function ($key_1, $key_2) { + if ($key_1 === 'c' && $key_2 === 1) { + // Red is on 0 in first iterator, and on 1 in second. + return 0; // Let's consider them the same. + } + + return $key_1 <=> $key_2; + }; + + $iterator_1 = new ArrayIterator(['a' => 'green', 'b' => 'brown', 'c' => 'blue', 'red']); + $iterator_2 = new ArrayIterator(['a' => 'green', 'yellow', 'blue']); + + $result = iterator_intersect_uassoc($iterator_1, $iterator_2, $compare_func); + + expect(iterator_to_array($result))->toBe(['a' => 'green', 'c' => 'blue']); +}); + +/** + * Tests for {@see iterator_intersect_ukey()}. + */ + +it('can intersect by key using a callback', function () { + $compare_func = function ($key_1, $key_2) { + // green is always different + if ($key_1 === $key_2 && !in_array('green', [$key_1, $key_2], true)) { + return 0; + } + + return 1; + }; + $iterator_1 = new ArrayIterator(['blue' => 1, 'red' => 2, 'green' => 3, 'purple' => 4]); + $iterator_2 = new ArrayIterator(['green' => 5, 'blue' => 6, 'yellow' => 7, 'cyan' => 8]); + + $result = iterator_intersect_ukey($iterator_1, $iterator_2, $compare_func); + expect(iterator_to_array($result))->toBe(['blue' => 1]); +}); + +it('intersects two iterators using a callback', function () { + $iterator_1 = new ArrayIterator([1, 2, 3]); + $iterator_2 = new ArrayIterator([2, 3, 4]); + + $callback = static function ($value_1, $value_2): int { + // 2 is always different + if ($value_1 === $value_2 && !in_array(2, [$value_1, $value_2], true)) { + return 0; + } + + return 1; + }; + + $iterator_intersect = iterator_uintersect($iterator_1, $iterator_2, $callback); + $iterator_intersect_2 = iterator_uintersect($iterator_2, $iterator_1, $callback); + + expect(iterator_to_array($iterator_intersect))->toBe([2 => 3]); + expect(iterator_to_array($iterator_intersect_2))->toBe([1 => 3]); +}); + +/** + * Test for {@see iterator_uintersect_assoc()}. + */ +it('can intersect using a callback and a key validation', function () { + $compare_func = function ($value_1, $value_2) { + if ($value_1 === 'apple' && $value_2 === 'orange') { + return 0; // nothing to compare, same fruit + } + + return $value_1 <=> $value_2; + }; + + $iterator_1 = new ArrayIterator(['apple', 'pear', 'lime']); + $iterator_2 = new ArrayIterator(['orange', 'berry']); + + $result = iterator_uintersect_assoc($iterator_1, $iterator_2, $compare_func); + expect(iterator_to_array($result))->toBe(['apple']); +}); + +/** + * Test for {@see iterator_uintersect_uassoc()}. + */ +it('can diff using a callback and a key validation using a callback', function () { + $value_compare = function ($value_1, $value_2) { + if ($value_1 === 'apple' && $value_2 === 'orange') { + return 0; // nothing to compare, same fruit + } + + return $value_1 <=> $value_2; + }; + + $key_compare = function ($value_1, $value_2) { + if ($value_1 === 0 && $value_2 === 1) { + return 0; // nothing to compare, same fruit + } + + return $value_1 <=> $value_2; + }; + + $iterator_1 = new ArrayIterator(['apple', 'pear', 'lime']); + $iterator_2 = new ArrayIterator(['berry', 'orange']); + + $result = iterator_uintersect_uassoc($iterator_1, $iterator_2, $value_compare, $key_compare); + expect(iterator_to_array($result))->toBe(['apple']); +}); + +test('exceptions are thrown', function (string $function, string $expected_message, array $args) { + expect(static fn () => $function(...$args))->toThrow($expected_message); +}) + ->with([ + ['iterator_intersect_uassoc', 'No associative callback provided.', [$it = new ArrayIterator(), $it]], + ['iterator_intersect_ukey', 'No key callback provided.', [$it, $it]], + ['iterator_uintersect', 'No intersect callback provided.', [$it, $it]], + ['iterator_uintersect_assoc', 'No intersect callback provided.', [$it, $it]], + ['iterator_uintersect_uassoc', 'No intersect callback provided.', [$it, $it]], + ['iterator_uintersect_uassoc', 'No associative callback provided.', [$it, $it, fn () => '']], + ]); diff --git a/tests/Functions/IteratorMapTest.php b/tests/Functions/IteratorMapTest.php index 4cc10a9..7ed9fce 100644 --- a/tests/Functions/IteratorMapTest.php +++ b/tests/Functions/IteratorMapTest.php @@ -1,6 +1,7 @@ toBeInstanceOf(MapIterator::class); expect(iterator_to_array($map_iterator))->toBe(['ONE', 'TWO', 'THREE']); }); + +it('maps with a callback via array params', function () { + $functions = new Functions(); + $map_iterator = iterator_map([$functions, 'strtoupper'], ['one', 'two', 'three']); + + expect($map_iterator)->toBeInstanceOf(MapIterator::class); + expect(iterator_to_array($map_iterator))->toBe(['ONE', 'TWO', 'THREE']); +}); diff --git a/tests/Iterator/DiffIteratorTest.php b/tests/Iterator/DiffIteratorTest.php new file mode 100644 index 0000000..347938f --- /dev/null +++ b/tests/Iterator/DiffIteratorTest.php @@ -0,0 +1,54 @@ +withKey()->withAssociative()); +})->expectExceptionObject(new \InvalidArgumentException( + 'Can only use one of "withKey" or "withAssociative", not both.' +)); + +it('can diff', function () { + $iterator_1 = new ArrayIterator(['one', 'two']); + $iterator_2 = new ArrayIterator(['one']); + $result = iterator_to_array(new DiffIterator($iterator_1, $iterator_2)); + + expect($result)->toBe([1 => 'two']); +}); + +it('can extract params form a function', function () { + $result = DiffIterator::extractParams([ + $iterator = new ArrayIterator(), + $iterator_clone = clone $iterator, + $iterator_clone, + $callable = fn () => '', + $callable, + ]); + + expect($result)->toBe([ + $iterator, + [$iterator_clone, $iterator_clone], + [$callable, $callable], + ]); +}); + +it('throws an exception if the first parameter is not an iterator', function () { + DiffIterator::extractParams([fn () => '']); +})->expectExceptionObject(new \InvalidArgumentException('First parameter must be an iterator.')); + +it('throws an exception if a parameter is not a callable or iterator.', function () { + DiffIterator::extractParams([new ArrayIterator(), 'invalid']); +})->expectExceptionObject(new \InvalidArgumentException('Argument should be an iterator or callback; "invalid" given.')); + +it('throws an exception if there is no iterator to compare against.', function () { + DiffIterator::extractParams([new ArrayIterator()]); +})->expectExceptionObject(new \InvalidArgumentException('There is no iterator to match against.')); + +it('throws an exception if an iterator is added after a callback.', function () { + DiffIterator::extractParams([new ArrayIterator(), new ArrayIterator(), fn () => '', new ArrayIterator()]); +})->expectExceptionObject(new \InvalidArgumentException('An iterator may not be provided after a callback.')); diff --git a/tests/Iterator/IntersectIteratorTest.php b/tests/Iterator/IntersectIteratorTest.php new file mode 100644 index 0000000..2b2b3eb --- /dev/null +++ b/tests/Iterator/IntersectIteratorTest.php @@ -0,0 +1,18 @@ +toBe([0 => 'one']); +}); diff --git a/tests/Iterator/ValuesIteratorTest.php b/tests/Iterator/ValuesIteratorTest.php index 25df90e..8a348a9 100644 --- a/tests/Iterator/ValuesIteratorTest.php +++ b/tests/Iterator/ValuesIteratorTest.php @@ -5,6 +5,14 @@ /** * Tests for {@see ValuesIterator}. */ + +it('works like array_values', function () { + $array = ['name', null, 'age', '']; + $result_array = array_values($array); + + expect(iterator_to_array(new ValuesIterator(new ArrayIterator($array))))->toBe($result_array); +}); + it('returns only the keys of the iterator', function () { $iterator = new \ArrayIterator(['one' => 1, 'two' => 2]); $iterator_values = new ValuesIterator($iterator);