From 1c9fac4b805aa9934c20c20d010e0757eaec118a Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sat, 14 Aug 2021 15:37:32 +0200 Subject: [PATCH 01/13] Initial commit of diff & intersect functions. --- src/Iterator/DiffIterator.php | 73 +++++++++++++++++++ src/Iterator/IntersectIterator.php | 14 ++++ src/iterator_functions.php | 54 ++++++++++++++ tests/Functions/IteratorDiffAssocTest.php | 13 ++++ tests/Functions/IteratorDiffTest.php | 26 +++++++ .../Functions/IteratorIntersectAssocTest.php | 13 ++++ tests/Functions/IteratorIntersectTest.php | 25 +++++++ 7 files changed, 218 insertions(+) create mode 100644 src/Iterator/DiffIterator.php create mode 100644 src/Iterator/IntersectIterator.php create mode 100644 tests/Functions/IteratorDiffAssocTest.php create mode 100644 tests/Functions/IteratorDiffTest.php create mode 100644 tests/Functions/IteratorIntersectAssocTest.php create mode 100644 tests/Functions/IteratorIntersectTest.php diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php new file mode 100644 index 0000000..f6a793c --- /dev/null +++ b/src/Iterator/DiffIterator.php @@ -0,0 +1,73 @@ +iterator_compare = new \AppendIterator(); + + foreach ($iterators as $iterator_compare) { + $this->iterator_compare->append($iterator_compare); + } + } + + /** + * @inheritdoc + * @since $ver$ + */ + public function accept(): bool + { + foreach ($this->iterator_compare as $key => $compare) { + if ($compare === $this->current()) { + if ($this->with_associative && $key !== $this->key()) { + continue; + } + + return $this->equal_accept; + } + } + + return !$this->equal_accept; + } + + /** + * Sets the iterator whether to compare with an extra key check. + * @param bool $bool Whether the iterator should be compared with extra key check. + * @return $this The iterator. + */ + public function withAssociative(bool $bool): self + { + $this->with_associative = $bool; + + return $this; + } +} 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 @@ +withAssociative(true); + } +} + if (!function_exists('iterator_filter')) { /** * Filters elements off an iterator using a callback function. @@ -47,6 +75,32 @@ 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(true); + } +} + if (!function_exists('iterator_keys')) { /** * Returns an iterator that produces only the keys of the inner iterator. diff --git a/tests/Functions/IteratorDiffAssocTest.php b/tests/Functions/IteratorDiffAssocTest.php new file mode 100644 index 0000000..059c27e --- /dev/null +++ b/tests/Functions/IteratorDiffAssocTest.php @@ -0,0 +1,13 @@ + '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']); +}); diff --git a/tests/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php new file mode 100644 index 0000000..a2889fd --- /dev/null +++ b/tests/Functions/IteratorDiffTest.php @@ -0,0 +1,26 @@ +toBe([0 => 1]); + expect(iterator_to_array($iterator_diff_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']); + + $iterator_diff = iterator_diff($iterator_1, $iterator_2, $iterator_3); + + expect(iterator_to_array($iterator_diff))->toBe([0 => 1, 3 => 'one', 4 => 'two']); +}); \ No newline at end of file diff --git a/tests/Functions/IteratorIntersectAssocTest.php b/tests/Functions/IteratorIntersectAssocTest.php new file mode 100644 index 0000000..3e41404 --- /dev/null +++ b/tests/Functions/IteratorIntersectAssocTest.php @@ -0,0 +1,13 @@ + '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']); +}); diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php new file mode 100644 index 0000000..89f6349 --- /dev/null +++ b/tests/Functions/IteratorIntersectTest.php @@ -0,0 +1,25 @@ +toBe([1 => 2, 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']); +}); \ No newline at end of file From ced7d09d837f2289014b5c672404ff5af376dabc Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sat, 14 Aug 2021 20:43:29 +0200 Subject: [PATCH 02/13] added `_key` functions for `_diff` and `_intersect`. --- src/Iterator/DiffIterator.php | 30 ++++++++++++++- src/iterator_functions.php | 25 ++++++++++++ tests/Functions/IteratorDiffAssocTest.php | 13 ------- tests/Functions/IteratorDiffTest.php | 38 +++++++++++++++---- .../Functions/IteratorIntersectAssocTest.php | 13 ------- tests/Functions/IteratorIntersectTest.php | 25 ++++++++++++ 6 files changed, 109 insertions(+), 35 deletions(-) delete mode 100644 tests/Functions/IteratorDiffAssocTest.php delete mode 100644 tests/Functions/IteratorIntersectAssocTest.php diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php index f6a793c..22c93bc 100644 --- a/src/Iterator/DiffIterator.php +++ b/src/Iterator/DiffIterator.php @@ -25,6 +25,12 @@ class DiffIterator extends \FilterIterator */ private bool $with_associative = false; + /** + * Whether the key should be compared instead of the value. + * @var bool + */ + private bool $with_key = false; + /** * @inheritdoc * @since $ver$ @@ -46,8 +52,16 @@ public function __construct(\Iterator $iterator, \Iterator ...$iterators) */ public function accept(): bool { - foreach ($this->iterator_compare as $key => $compare) { - if ($compare === $this->current()) { + if ($this->with_key && $this->with_associative) { + throw new \InvalidArgumentException('Can only use one of "withKey" or "withAssociative", not both.'); + } + + foreach ($this->iterator_compare as $key => $value) { + if ($this->with_key && $key === $this->key()) { + return $this->equal_accept; + } + + if ($value === $this->current()) { if ($this->with_associative && $key !== $this->key()) { continue; } @@ -70,4 +84,16 @@ public function withAssociative(bool $bool): self return $this; } + + /** + * Sets the iterator whether to compare against the key. + * @param bool $bool Whether the iterator should be compared against the key. + * @return $this The iterator. + */ + public function withKey(bool $bool): self + { + $this->with_key = $bool; + + return $this; + } } diff --git a/src/iterator_functions.php b/src/iterator_functions.php index b750e3e..85f363f 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -47,6 +47,19 @@ function iterator_diff_assoc(\Iterator $iterator, \Iterator ...$iterators): Diff } } +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(true); + } +} + if (!function_exists('iterator_filter')) { /** * Filters elements off an iterator using a callback function. @@ -100,6 +113,18 @@ function iterator_intersect_assoc(\Iterator $iterator, \Iterator ...$iterators): return (new IntersectIterator($iterator, ...$iterators))->withAssociative(true); } } +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(true); + } +} if (!function_exists('iterator_keys')) { /** diff --git a/tests/Functions/IteratorDiffAssocTest.php b/tests/Functions/IteratorDiffAssocTest.php deleted file mode 100644 index 059c27e..0000000 --- a/tests/Functions/IteratorDiffAssocTest.php +++ /dev/null @@ -1,13 +0,0 @@ - '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']); -}); diff --git a/tests/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php index a2889fd..472a28e 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -8,11 +8,11 @@ $iterator_1 = new ArrayIterator([1, 2, 3]); $iterator_2 = new ArrayIterator([2, 3, 4]); - $iterator_diff = iterator_diff($iterator_1, $iterator_2); - $iterator_diff_2 = iterator_diff($iterator_2, $iterator_1); + $result_1 = iterator_diff($iterator_1, $iterator_2); + $result_2 = iterator_diff($iterator_2, $iterator_1); - expect(iterator_to_array($iterator_diff))->toBe([0 => 1]); - expect(iterator_to_array($iterator_diff_2))->toBe([2 => 4]); + expect(iterator_to_array($result_1))->toBe([0 => 1]); + expect(iterator_to_array($result_2))->toBe([2 => 4]); }); it('diffs more than two iterators', function () { @@ -20,7 +20,31 @@ $iterator_2 = new ArrayIterator([2, 3, 4]); $iterator_3 = new ArrayIterator(['three', 'four', 'five']); - $iterator_diff = iterator_diff($iterator_1, $iterator_2, $iterator_3); + $result = iterator_diff($iterator_1, $iterator_2, $iterator_3); - expect(iterator_to_array($iterator_diff))->toBe([0 => 1, 3 => 'one', 4 => 'two']); -}); \ No newline at end of file + 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]); +}); diff --git a/tests/Functions/IteratorIntersectAssocTest.php b/tests/Functions/IteratorIntersectAssocTest.php deleted file mode 100644 index 3e41404..0000000 --- a/tests/Functions/IteratorIntersectAssocTest.php +++ /dev/null @@ -1,13 +0,0 @@ - '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']); -}); diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index 89f6349..7add3ec 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -22,4 +22,29 @@ $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]); }); \ No newline at end of file From 826fc252f554abde747446eeb5c688550561a8d3 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sun, 15 Aug 2021 07:58:55 +0200 Subject: [PATCH 03/13] Added `_ukey` for `_diff` and `_intersect`. --- src/Iterator/DiffIterator.php | 50 ++++++++++++++++++----- src/iterator_functions.php | 37 +++++++++++++++-- tests/Functions/IteratorDiffTest.php | 19 +++++++++ tests/Functions/IteratorIntersectTest.php | 24 ++++++++++- 4 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php index 22c93bc..b9860a8 100644 --- a/src/Iterator/DiffIterator.php +++ b/src/Iterator/DiffIterator.php @@ -27,13 +27,12 @@ class DiffIterator extends \FilterIterator /** * Whether the key should be compared instead of the value. - * @var bool + * @var null|callable */ - private bool $with_key = false; + private $key_compare; /** * @inheritdoc - * @since $ver$ */ public function __construct(\Iterator $iterator, \Iterator ...$iterators) { @@ -46,18 +45,49 @@ public function __construct(\Iterator $iterator, \Iterator ...$iterators) } } + /** + * Extracts the params from a function call. + * @param array $arguments The provided arguments. + * @return mixed The params. + */ + public static function extractParams(array $arguments): array + { + $result = ['iterator' => null, 'iterators' => [], 'callbacks' => []]; + + $iterator = array_shift($arguments); + if (!$iterator instanceof \Iterator) { + throw new \InvalidArgumentException('First parameter must be an iterator.'); + } + + $result['iterator'] = $iterator; + + while (($argument = array_shift($arguments))) { + 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'; + $result[$type][] = $argument; + } + + return array_values($result); + } + /** * @inheritdoc - * @since $ver$ */ public function accept(): bool { - if ($this->with_key && $this->with_associative) { + if ($this->key_compare && $this->with_associative) { throw new \InvalidArgumentException('Can only use one of "withKey" or "withAssociative", not both.'); } foreach ($this->iterator_compare as $key => $value) { - if ($this->with_key && $key === $this->key()) { + if ($this->key_compare && ($this->key_compare)($this->key(), $key) === 0) { return $this->equal_accept; } @@ -86,13 +116,13 @@ public function withAssociative(bool $bool): self } /** - * Sets the iterator whether to compare against the key. - * @param bool $bool Whether the iterator should be compared against the key. + * 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(bool $bool): self + public function withKey(?callable $callback = null): self { - $this->with_key = $bool; + $this->key_compare = $callback ?? static fn($current_key, $compare_key):int => $current_key <=> $compare_key; return $this; } diff --git a/src/iterator_functions.php b/src/iterator_functions.php index 85f363f..de8048f 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -56,7 +56,23 @@ function iterator_diff_assoc(\Iterator $iterator, \Iterator ...$iterators): Diff */ function iterator_diff_key(\Iterator $iterator, \Iterator ...$iterators): DiffIterator { - return (new DiffIterator($iterator, ...$iterators))->withKey(true); + return (new DiffIterator($iterator, ...$iterators))->withKey(); + } +} + +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()); + + return (new DiffIterator($iterator, ...$iterators))->withKey(...$callbacks); } } @@ -72,7 +88,7 @@ function iterator_diff_key(\Iterator $iterator, \Iterator ...$iterators): DiffIt */ 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)); } } @@ -122,7 +138,22 @@ function iterator_intersect_assoc(\Iterator $iterator, \Iterator ...$iterators): */ function iterator_intersect_key(\Iterator $iterator, \Iterator ...$iterators): DiffIterator { - return (new IntersectIterator($iterator, ...$iterators))->withKey(true); + return (new IntersectIterator($iterator, ...$iterators))->withKey(); + } +} + +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. + * @return DiffIterator An iterator with the difference. + */ + function iterator_intersect_ukey(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + + return (new IntersectIterator($iterator, ...$iterators))->withKey(...$callbacks); } } diff --git a/tests/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php index 472a28e..11dc2aa 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -48,3 +48,22 @@ $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]); +}); \ No newline at end of file diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index 7add3ec..f92e12b 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -38,7 +38,7 @@ }); /** -* Tests for {@see iterator_intersect_key()}. + * Tests for {@see iterator_intersect_key()}. */ it('can intersect by key', function () { @@ -47,4 +47,24 @@ $result = iterator_intersect_key($iterator_1, $iterator_2); expect(iterator_to_array($result))->toBe(['blue' => 1, 'green' => 3]); -}); \ No newline at end of file +}); + +/** + * 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]); +}); From 6aa8e4a96caa2feb1143d032558b24b0ece24760 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sun, 15 Aug 2021 08:16:36 +0200 Subject: [PATCH 04/13] added `iterator_udiff` and `iterator_uintersect` --- src/Iterator/DiffIterator.php | 25 +++++++++++++++--- src/iterator_functions.php | 31 +++++++++++++++++++++++ tests/Functions/IteratorDiffTest.php | 21 ++++++++++++++- tests/Functions/IteratorIntersectTest.php | 20 +++++++++++++++ 4 files changed, 93 insertions(+), 4 deletions(-) diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php index b9860a8..235eccc 100644 --- a/src/Iterator/DiffIterator.php +++ b/src/Iterator/DiffIterator.php @@ -26,11 +26,17 @@ class DiffIterator extends \FilterIterator private bool $with_associative = false; /** - * Whether the key should be compared instead of the value. + * Callback to use for key comparison. * @var null|callable */ private $key_compare; + /** + * callback to use for value comparison. + * @var null|callable + */ + private $value_compare; + /** * @inheritdoc */ @@ -39,6 +45,7 @@ public function __construct(\Iterator $iterator, \Iterator ...$iterators) parent::__construct($iterator); $this->iterator_compare = new \AppendIterator(); + $this->value_compare = static fn($current_value, $compare_value):int => $current_value <=> $compare_value; foreach ($iterators as $iterator_compare) { $this->iterator_compare->append($iterator_compare); @@ -91,7 +98,7 @@ public function accept(): bool return $this->equal_accept; } - if ($value === $this->current()) { + if (($this->value_compare)($this->current(), $value) === 0) { if ($this->with_associative && $key !== $this->key()) { continue; } @@ -122,7 +129,19 @@ public function withAssociative(bool $bool): self */ public function withKey(?callable $callback = null): self { - $this->key_compare = $callback ?? static fn($current_key, $compare_key):int => $current_key <=> $compare_key; + $this->key_compare = $callback ?? static fn($current_key, $compare_key): int => $current_key <=> $compare_key; + + 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; } diff --git a/src/iterator_functions.php b/src/iterator_functions.php index de8048f..f310d99 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -76,6 +76,22 @@ function iterator_diff_ukey(): DiffIterator } } +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 callback(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()); + + return (new DiffIterator($iterator, ...$iterators))->withCallback(...$callbacks); + } +} + if (!function_exists('iterator_filter')) { /** * Filters elements off an iterator using a callback function. @@ -156,6 +172,21 @@ function iterator_intersect_ukey(): DiffIterator 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 callback(mixed $current_value, mixed $compare_value):int The callback to perform. + * @return DiffIterator An iterator with the difference. + */ + function iterator_uintersect(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + + return (new IntersectIterator($iterator, ...$iterators))->withCallback(...$callbacks); + } +} if (!function_exists('iterator_keys')) { /** diff --git a/tests/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php index 11dc2aa..53728e0 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -66,4 +66,23 @@ $result = iterator_diff_ukey($iterator_1, $iterator_2, $compare_func); expect(iterator_to_array($result))->toBe(['red' => 2, 'green' => 3, 'purple' => 4]); -}); \ No newline at end of file +}); + +/** + * 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]); +}); diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index f92e12b..e44dfe1 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -68,3 +68,23 @@ $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]); +}); From 61df35f20c1a5f6f45720ac99549d9f1f3f34bad Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sun, 15 Aug 2021 12:08:01 +0200 Subject: [PATCH 05/13] Added `_uassoc` for `diff` and `intersect` --- src/Iterator/DiffIterator.php | 29 +++++++++++------ src/iterator_functions.php | 39 +++++++++++++++++++++-- tests/Functions/IteratorDiffTest.php | 18 +++++++++++ tests/Functions/IteratorIntersectTest.php | 22 +++++++++++++ 4 files changed, 95 insertions(+), 13 deletions(-) diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php index 235eccc..7ec4683 100644 --- a/src/Iterator/DiffIterator.php +++ b/src/Iterator/DiffIterator.php @@ -20,10 +20,10 @@ class DiffIterator extends \FilterIterator protected bool $equal_accept = false; /** - * Whether the value should be compared including the key. - * @var bool + * Callback to use for _assoc comparing. + * @var null|callable */ - private bool $with_associative = false; + private $assoc_compare; /** * Callback to use for key comparison. @@ -45,7 +45,7 @@ public function __construct(\Iterator $iterator, \Iterator ...$iterators) parent::__construct($iterator); $this->iterator_compare = new \AppendIterator(); - $this->value_compare = static fn($current_value, $compare_value):int => $current_value <=> $compare_value; + $this->value_compare = self::defaultCompare(); foreach ($iterators as $iterator_compare) { $this->iterator_compare->append($iterator_compare); @@ -89,7 +89,7 @@ public static function extractParams(array $arguments): array */ public function accept(): bool { - if ($this->key_compare && $this->with_associative) { + if ($this->key_compare && $this->assoc_compare) { throw new \InvalidArgumentException('Can only use one of "withKey" or "withAssociative", not both.'); } @@ -99,7 +99,7 @@ public function accept(): bool } if (($this->value_compare)($this->current(), $value) === 0) { - if ($this->with_associative && $key !== $this->key()) { + if ($this->assoc_compare && (($this->assoc_compare)($this->key(), $key) !== 0)) { continue; } @@ -112,12 +112,12 @@ public function accept(): bool /** * Sets the iterator whether to compare with an extra key check. - * @param bool $bool Whether the iterator should be compared with extra key check. + * @param callable|null $callback Optional callback to use for comparison. * @return $this The iterator. */ - public function withAssociative(bool $bool): self + public function withAssociative(?callable $callback = null): self { - $this->with_associative = $bool; + $this->assoc_compare = $callback ?? self::defaultCompare(); return $this; } @@ -129,7 +129,7 @@ public function withAssociative(bool $bool): self */ public function withKey(?callable $callback = null): self { - $this->key_compare = $callback ?? static fn($current_key, $compare_key): int => $current_key <=> $compare_key; + $this->key_compare = $callback ?? self::defaultCompare(); return $this; } @@ -145,4 +145,13 @@ public function withCallback(callable $callback): self 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_functions.php b/src/iterator_functions.php index f310d99..17a8bc2 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -43,7 +43,7 @@ function iterator_diff(\Iterator $iterator, \Iterator ...$iterators): DiffIterat */ function iterator_diff_assoc(\Iterator $iterator, \Iterator ...$iterators): DiffIterator { - return (new DiffIterator($iterator, ...$iterators))->withAssociative(true); + return (new DiffIterator($iterator, ...$iterators))->withAssociative(); } } @@ -60,6 +60,22 @@ function iterator_diff_key(\Iterator $iterator, \Iterator ...$iterators): DiffIt } } +if (!function_exists('iterator_diff_uassoc')) { + /** + * 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_uassoc(): DiffIterator + { + [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + + return (new DiffIterator($iterator, ...$iterators))->withAssociative(...$callbacks); + } +} + if (!function_exists('iterator_diff_ukey')) { /** * Computes the difference of iterators by key check using a callback. @@ -142,7 +158,7 @@ function iterator_intersect(\Iterator $iterator, \Iterator ...$iterators): Inter */ function iterator_intersect_assoc(\Iterator $iterator, \Iterator ...$iterators): IntersectIterator { - return (new IntersectIterator($iterator, ...$iterators))->withAssociative(true); + return (new IntersectIterator($iterator, ...$iterators))->withAssociative(); } } if (!function_exists('iterator_intersect_key')) { @@ -158,16 +174,33 @@ function iterator_intersect_key(\Iterator $iterator, \Iterator ...$iterators): D } } +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()); + + 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 DiffIterator An iterator with the difference. */ function iterator_intersect_ukey(): DiffIterator { - [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); return (new IntersectIterator($iterator, ...$iterators))->withKey(...$callbacks); } diff --git a/tests/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php index 53728e0..4e999dc 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -68,6 +68,24 @@ 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()}. */ diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index e44dfe1..1889b54 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -49,6 +49,28 @@ 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()}. */ From e21c4ace1643efaefceac3c58e00bce6aa4a2322 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sun, 15 Aug 2021 15:11:15 +0200 Subject: [PATCH 06/13] Added `_assoc` & `_uassoc` for `_udiff` and `_uintersect`. --- src/iterator_functions.php | 89 +++++++++++++++++++++-- tests/Functions/IteratorDiffTest.php | 44 +++++++++++ tests/Functions/IteratorIntersectTest.php | 44 +++++++++++ 3 files changed, 170 insertions(+), 7 deletions(-) diff --git a/src/iterator_functions.php b/src/iterator_functions.php index 17a8bc2..11813e7 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -97,7 +97,7 @@ function iterator_diff_ukey(): DiffIterator * 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 callback(mixed $current_value, mixed $compare_value):int The callback to perform. + * @param callable(mixed $current_value, mixed $compare_value):int The callback to perform. * @return DiffIterator An iterator with the difference. */ function iterator_udiff(): DiffIterator @@ -108,6 +108,43 @@ function iterator_udiff(): DiffIterator } } +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()); + + 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()); + + 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. @@ -196,31 +233,69 @@ function iterator_intersect_uassoc(): IntersectIterator * @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 DiffIterator An iterator with the difference. + * @return IntersectIterator An iterator with the intersection. */ - function iterator_intersect_ukey(): DiffIterator + function iterator_intersect_ukey(): IntersectIterator { [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); 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 callback(mixed $current_value, mixed $compare_value):int The callback to perform. - * @return DiffIterator An iterator with the difference. + * @param callable(mixed $current_value, mixed $compare_value):int The callback to perform. + * @return IntersectIterator An iterator with the intersection. */ - function iterator_uintersect(): DiffIterator + function iterator_uintersect(): IntersectIterator { - [$iterator, $iterators, $callbacks] = DiffIterator::extractParams(func_get_args()); + [$iterator, $iterators, $callbacks] = IntersectIterator::extractParams(func_get_args()); 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()); + + 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()); + + 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/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php index 4e999dc..76cc94e 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -104,3 +104,47 @@ $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']); +}); + diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index 1889b54..7793a34 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -110,3 +110,47 @@ 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']); +}); + From 5a40de3ce2dd80fa7985685dc185a83ab0e06d27 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Mon, 16 Aug 2021 21:54:07 +0200 Subject: [PATCH 07/13] Added bunch of tests & exceptions. Fixed ColumnIterator. --- src/Iterator/ColumnIterator.php | 38 ++++++++++++++-- src/Iterator/DiffIterator.php | 16 +++++-- src/Iterator/MapIterator.php | 2 +- src/iterator_functions.php | 41 ++++++++++++++++- tests/Functions/IteratorIntersectTest.php | 12 ++++- tests/Iterator/ColumnIteratorTest.php | 20 ++++++++- tests/Iterator/DiffIteratorTest.php | 54 +++++++++++++++++++++++ tests/Iterator/IntersectIteratorTest.php | 18 ++++++++ tests/Iterator/ValuesIteratorTest.php | 8 ++++ 9 files changed, 197 insertions(+), 12 deletions(-) create mode 100644 tests/Iterator/DiffIteratorTest.php create mode 100644 tests/Iterator/IntersectIteratorTest.php diff --git a/src/Iterator/ColumnIterator.php b/src/Iterator/ColumnIterator.php index 650cf7a..ba2f8ef 100644 --- a/src/Iterator/ColumnIterator.php +++ b/src/Iterator/ColumnIterator.php @@ -5,7 +5,7 @@ /** * Iterator that returns a single column for the iteration array / object. */ -class ColumnIterator extends \IteratorIterator +class ColumnIterator extends \FilterIterator { /** * The column to return. @@ -19,6 +19,12 @@ class ColumnIterator extends \IteratorIterator */ private $index_key; + /** + * The internal key count. + * @var int + */ + private int $count = 0; + /** * Creates the iterator. * @param \Traversable $iterator The iterator that provides the arrays / objects. @@ -33,13 +39,25 @@ public function __construct(\Traversable $iterator, $column_key, $index_key = nu $this->index_key = $index_key; } + /** + * @inheritdoc + */ + public function next(): void + { + parent::next(); + + if (isset($this->index_key) && !$this->column($this->index_key)) { + ++$this->count; + } + } + /** * @inheritdoc */ public function key() { if (isset($this->index_key)) { - return $this->column($this->index_key); + return $this->column($this->index_key) ?? $this->count; } return parent::key(); @@ -66,13 +84,25 @@ protected function column($key) { $iteration = parent::current(); if (is_array($iteration) || $iteration instanceof \ArrayAccess) { - return $iteration[$key]; + return $iteration[$key] ?? null; } if (is_object($iteration)) { - return $iteration->{$key}; + return $iteration->{$key} ?? null; } return null; } + + /** + * @inheritdoc + */ + public function accept(): bool + { + if (isset($this->column_key)) { + return $this->column($this->column_key) !== null; + } + + return true; + } } diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php index 7ec4683..ea12dbf 100644 --- a/src/Iterator/DiffIterator.php +++ b/src/Iterator/DiffIterator.php @@ -54,21 +54,21 @@ public function __construct(\Iterator $iterator, \Iterator ...$iterators) /** * Extracts the params from a function call. - * @param array $arguments The provided arguments. + * @param array $params The provided arguments. * @return mixed The params. */ - public static function extractParams(array $arguments): array + final public static function extractParams(array $params): array { $result = ['iterator' => null, 'iterators' => [], 'callbacks' => []]; - $iterator = array_shift($arguments); + $iterator = array_shift($params); if (!$iterator instanceof \Iterator) { throw new \InvalidArgumentException('First parameter must be an iterator.'); } $result['iterator'] = $iterator; - while (($argument = array_shift($arguments))) { + 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.', @@ -78,9 +78,17 @@ public static function extractParams(array $arguments): array $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); } diff --git a/src/Iterator/MapIterator.php b/src/Iterator/MapIterator.php index 4815767..a2654b6 100644 --- a/src/Iterator/MapIterator.php +++ b/src/Iterator/MapIterator.php @@ -23,7 +23,7 @@ public function __construct(callable $callback, iterable ...$iterators) try { $function = new \ReflectionFunction(\Closure::fromCallable($callback)); } catch (\ReflectionException $e) { - throw new \RuntimeException($e->getMessage(), (int) $e->getCode(), $e); + return; // Will not happen. } if (!$function->isVariadic() && $function->getNumberOfParameters() !== count($iterators)) { diff --git a/src/iterator_functions.php b/src/iterator_functions.php index 11813e7..1b5f2bc 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -62,7 +62,7 @@ function iterator_diff_key(\Iterator $iterator, \Iterator ...$iterators): DiffIt if (!function_exists('iterator_diff_uassoc')) { /** - * Computes the difference of iterators by key check using a callback. + * 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`. @@ -71,6 +71,9 @@ function iterator_diff_key(\Iterator $iterator, \Iterator ...$iterators): DiffIt 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); } @@ -87,6 +90,9 @@ function iterator_diff_uassoc(): DiffIterator 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); } @@ -103,6 +109,9 @@ function iterator_diff_ukey(): DiffIterator 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); } @@ -119,6 +128,9 @@ function iterator_udiff(): DiffIterator 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) @@ -138,6 +150,13 @@ function iterator_udiff_assoc(): DiffIterator 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]) @@ -222,6 +241,9 @@ function iterator_intersect_key(\Iterator $iterator, \Iterator ...$iterators): D 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); } @@ -238,6 +260,9 @@ function iterator_intersect_uassoc(): IntersectIterator 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); } @@ -254,6 +279,9 @@ function iterator_intersect_ukey(): IntersectIterator 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); } @@ -270,6 +298,9 @@ function iterator_uintersect(): IntersectIterator 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) @@ -290,6 +321,14 @@ 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]); diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index 7793a34..0686243 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -14,6 +14,17 @@ 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]); @@ -153,4 +164,3 @@ $result = iterator_uintersect_uassoc($iterator_1, $iterator_2, $value_compare, $key_compare); expect(iterator_to_array($result))->toBe(['apple']); }); - diff --git a/tests/Iterator/ColumnIteratorTest.php b/tests/Iterator/ColumnIteratorTest.php index cea58c8..8fe48db 100644 --- a/tests/Iterator/ColumnIteratorTest.php +++ b/tests/Iterator/ColumnIteratorTest.php @@ -44,7 +44,7 @@ ]); }); -it('returns a single column for an iterator with objects', function() { +it('returns a single column for an iterator with objects', function () { $titles = [ (object) ['id' => 1, 'title' => 'Title 1'], (object) ['id' => 2, 'title' => 'Title 2'], @@ -55,3 +55,21 @@ expect(iterator_to_array($column_iterator))->toBe([1 => 'Title 1', 2 => 'Title 2']); }); + +it('contains integer keys when the key is missing', function () { + // make sure array_column and iterator_column act the same + $array = [['age' => 32], ['name' => 'Doeke', 'age' => 33], ['age' => 45]]; + $result_array = array_column($array, 'age', 'name'); + $result_iterator = new ColumnIterator(new ArrayIterator($array), 'age', 'name'); + + expect($result_array)->toBe(iterator_to_array($result_iterator)); +}); + +it('skips empty values the value-key is missing', function () { + // make sure array_column and iterator_column act the same + $array = [['age' => 32], ['name' => 'Doeke', 'age' => 33], ['age' => 45]]; + $result_array = array_column($array, 'name', 'age'); + $result_iterator = new ColumnIterator(new ArrayIterator($array), 'name', 'age'); + + expect($result_array)->toBe(iterator_to_array($result_iterator)); +}); \ No newline at end of file diff --git a/tests/Iterator/DiffIteratorTest.php b/tests/Iterator/DiffIteratorTest.php new file mode 100644 index 0000000..e2af225 --- /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..bb12203 --- /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); From 6919e5b3cd27bf5c10e776ce9518982d440b32ee Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Tue, 24 Aug 2021 22:00:33 +0200 Subject: [PATCH 08/13] Code style --- .gitignore | 3 +- .php_cs.dist.php | 40 +++++++++++++++++++++++ src/Iterator/DiffIterator.php | 2 +- src/iterator_functions.php | 2 +- tests/Functions/IteratorColumnTest.php | 2 +- tests/Functions/IteratorDiffTest.php | 12 ++++--- tests/Functions/IteratorFilterTest.php | 6 ++-- tests/Functions/IteratorIntersectTest.php | 3 ++ tests/Functions/IteratorKeysTest.php | 4 +-- tests/Functions/IteratorMapTest.php | 2 +- tests/Functions/IteratorReduceTest.php | 2 +- tests/Functions/IteratorValuesTest.php | 2 +- tests/Functions/IteratorWalkTest.php | 1 - tests/Iterator/ColumnIteratorTest.php | 4 +-- tests/Iterator/DiffIteratorTest.php | 10 +++--- tests/Iterator/IntersectIteratorTest.php | 2 +- tests/Iterator/KeysIteratorTest.php | 6 ++-- tests/Iterator/MapIteratorTest.php | 4 +-- 18 files changed, 76 insertions(+), 31 deletions(-) create mode 100644 .php_cs.dist.php diff --git a/.gitignore b/.gitignore index b561abe..a3cd688 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /vendor/ composer.lock -.phpunit.result.cache \ No newline at end of file +.phpunit.result.cache +.php-cs-fixer.cache \ No newline at end of file diff --git a/.php_cs.dist.php b/.php_cs.dist.php new file mode 100644 index 0000000..2328504 --- /dev/null +++ b/.php_cs.dist.php @@ -0,0 +1,40 @@ +in([ + __DIR__ . '/src', + __DIR__ . '/tests', + ]) + ->name('*.php') + ->notName('*.blade.php') + ->ignoreDotFiles(true) + ->ignoreVCS(true); + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PSR12' => true, + 'array_syntax' => ['syntax' => 'short'], + 'ordered_imports' => ['sort_algorithm' => 'alpha'], + 'no_unused_imports' => true, +// 'not_operator_with_successor_space' => true, + 'trailing_comma_in_multiline' => true, + 'phpdoc_scalar' => true, +// 'unary_operator_spaces' => true, +// 'binary_operator_spaces' => true, + 'blank_line_before_statement' => [ + 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], + ], + 'phpdoc_single_line_var_spacing' => true, + 'phpdoc_var_without_name' => true, + 'class_attributes_separation' => [ + 'elements' => [ + 'method' => 'one', + ], + ], + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + 'keep_multiple_spaces_after_comma' => true, + ], + 'single_trait_insert_per_statement' => true, + ]) + ->setFinder($finder); diff --git a/src/Iterator/DiffIterator.php b/src/Iterator/DiffIterator.php index ea12dbf..673bd37 100644 --- a/src/Iterator/DiffIterator.php +++ b/src/Iterator/DiffIterator.php @@ -160,6 +160,6 @@ public function withCallback(callable $callback): self */ final protected static function defaultCompare(): callable { - return static fn($current, $compare) => $current <=> $compare; + return static fn ($current, $compare) => $current <=> $compare; } } diff --git a/src/iterator_functions.php b/src/iterator_functions.php index 1b5f2bc..ebd473c 100644 --- a/src/iterator_functions.php +++ b/src/iterator_functions.php @@ -176,7 +176,7 @@ function iterator_udiff_uassoc(): DiffIterator */ function iterator_filter(Iterator $iterator, ?callable $callback = null): \CallbackFilterIterator { - return new \CallbackFilterIterator($iterator, $callback ?? static fn($value): bool => !empty($value)); + return new \CallbackFilterIterator($iterator, $callback ?? static fn ($value): bool => !empty($value)); } } diff --git a/tests/Functions/IteratorColumnTest.php b/tests/Functions/IteratorColumnTest.php index a63e3d4..d12d031 100644 --- a/tests/Functions/IteratorColumnTest.php +++ b/tests/Functions/IteratorColumnTest.php @@ -22,4 +22,4 @@ $column_iterator = iterator_column($iterator, 'title'); expect($column_iterator)->toBeInstanceOf(ColumnIterator::class); -}); \ No newline at end of file +}); diff --git a/tests/Functions/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php index 76cc94e..af0bd8d 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -110,10 +110,11 @@ */ 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; + 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']); @@ -131,6 +132,7 @@ if ($value_1 === 'apple' && $value_2 === 'orange') { return 0; // nothing to compare, same fruit } + return $value_1 <=> $value_2; }; @@ -138,6 +140,7 @@ if ($value_1 === 0 && $value_2 === 1) { return 0; // nothing to compare, same fruit } + return $value_1 <=> $value_2; }; @@ -147,4 +150,3 @@ $result = iterator_udiff_uassoc($iterator_1, $iterator_2, $value_compare, $key_compare); expect(iterator_to_array($result))->toBe([1 => 'pear', 2 => 'lime']); }); - diff --git a/tests/Functions/IteratorFilterTest.php b/tests/Functions/IteratorFilterTest.php index 43980ac..7191ed3 100644 --- a/tests/Functions/IteratorFilterTest.php +++ b/tests/Functions/IteratorFilterTest.php @@ -5,7 +5,7 @@ */ it('filters an iterator', function () { $iterator = new \ArrayIterator(['one', 'two', 'three']); - $filtered = iterator_filter($iterator, fn(string $value) => $value !== 'two'); + $filtered = iterator_filter($iterator, fn (string $value) => $value !== 'two'); expect(iterator_to_array($filtered))->toBe([0 => 'one', 2 => 'three']); }); @@ -15,7 +15,7 @@ expect(iterator_to_array($filtered))->toBe([0 => 'one', 4 => 'three']); }); -it('returns a CallbackFilterIterator', function() { +it('returns a CallbackFilterIterator', function () { $iterator = iterator_filter(new \ArrayIterator()); expect($iterator)->toBeInstanceOf(\CallbackFilterIterator::class); -}); \ No newline at end of file +}); diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index 0686243..86990ac 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -130,6 +130,7 @@ if ($value_1 === 'apple' && $value_2 === 'orange') { return 0; // nothing to compare, same fruit } + return $value_1 <=> $value_2; }; @@ -148,6 +149,7 @@ if ($value_1 === 'apple' && $value_2 === 'orange') { return 0; // nothing to compare, same fruit } + return $value_1 <=> $value_2; }; @@ -155,6 +157,7 @@ if ($value_1 === 0 && $value_2 === 1) { return 0; // nothing to compare, same fruit } + return $value_1 <=> $value_2; }; diff --git a/tests/Functions/IteratorKeysTest.php b/tests/Functions/IteratorKeysTest.php index cc862ec..4809bc4 100644 --- a/tests/Functions/IteratorKeysTest.php +++ b/tests/Functions/IteratorKeysTest.php @@ -9,13 +9,13 @@ /** * Tests for {@see iterator_keys()). */ -it('returns only the keys of the iterator', function() { +it('returns only the keys of the iterator', function () { $iterator = new \ArrayIterator(['one' => 1, 'two' => 2]); $iterator_values = iterator_keys($iterator); expect(iterator_to_array($iterator_values))->toBe(['one', 'two']); }); -it('returns a KeysIterator', function() { +it('returns a KeysIterator', function () { $iterator_values = iterator_keys(new ArrayIterator()); expect($iterator_values)->toBeInstanceOf(KeysIterator::class); }); diff --git a/tests/Functions/IteratorMapTest.php b/tests/Functions/IteratorMapTest.php index 081853f..4cc10a9 100644 --- a/tests/Functions/IteratorMapTest.php +++ b/tests/Functions/IteratorMapTest.php @@ -18,4 +18,4 @@ expect($map_iterator)->toBeInstanceOf(MapIterator::class); expect(iterator_to_array($map_iterator))->toBe(['ONE', 'TWO', 'THREE']); -}); \ No newline at end of file +}); diff --git a/tests/Functions/IteratorReduceTest.php b/tests/Functions/IteratorReduceTest.php index 1272ccb..3d66ed4 100644 --- a/tests/Functions/IteratorReduceTest.php +++ b/tests/Functions/IteratorReduceTest.php @@ -14,7 +14,7 @@ it('reduces to null without values', function () { $iterator = new \ArrayIterator([]); - $result = iterator_reduce($iterator, static fn($carry, $value) => $value); + $result = iterator_reduce($iterator, static fn ($carry, $value) => $value); expect($result)->toBeNull(); }); diff --git a/tests/Functions/IteratorValuesTest.php b/tests/Functions/IteratorValuesTest.php index 49461af..fc96d9e 100644 --- a/tests/Functions/IteratorValuesTest.php +++ b/tests/Functions/IteratorValuesTest.php @@ -12,7 +12,7 @@ expect(iterator_to_array($iterator_values))->toBe([1, 2]); }); -it('returns a ValuesIterator', function() { +it('returns a ValuesIterator', function () { $iterator_values = iterator_values(new ArrayIterator()); expect($iterator_values)->toBeInstanceOf(ValuesIterator::class); }); diff --git a/tests/Functions/IteratorWalkTest.php b/tests/Functions/IteratorWalkTest.php index 64c57d3..389a656 100644 --- a/tests/Functions/IteratorWalkTest.php +++ b/tests/Functions/IteratorWalkTest.php @@ -17,4 +17,3 @@ expect($objects[0]->title)->toBe('Title 1.0.one.two'); expect($objects[1]->title)->toBe('Title 2.1.one.two'); }); - diff --git a/tests/Iterator/ColumnIteratorTest.php b/tests/Iterator/ColumnIteratorTest.php index 8fe48db..27812e2 100644 --- a/tests/Iterator/ColumnIteratorTest.php +++ b/tests/Iterator/ColumnIteratorTest.php @@ -1,6 +1,6 @@ toBe(iterator_to_array($result_iterator)); -}); \ No newline at end of file +}); diff --git a/tests/Iterator/DiffIteratorTest.php b/tests/Iterator/DiffIteratorTest.php index e2af225..347938f 100644 --- a/tests/Iterator/DiffIteratorTest.php +++ b/tests/Iterator/DiffIteratorTest.php @@ -1,6 +1,6 @@ '', + $callable = fn () => '', $callable, ]); expect($result)->toBe([ $iterator, [$iterator_clone, $iterator_clone], - [$callable, $callable] + [$callable, $callable], ]); }); it('throws an exception if the first parameter is not an iterator', function () { - DiffIterator::extractParams([fn() => '']); + 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 () { @@ -50,5 +50,5 @@ })->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()]); + 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 index bb12203..2b2b3eb 100644 --- a/tests/Iterator/IntersectIteratorTest.php +++ b/tests/Iterator/IntersectIteratorTest.php @@ -1,6 +1,6 @@ 1, 'two' => 2]); $iterator_values = new KeysIterator($iterator); expect(iterator_to_array($iterator_values))->toBe(['one', 'two']); }); -it('can be rewound', function() { +it('can be rewound', function () { $iterator = new \ArrayIterator(['one' => 1, 'two' => 2]); $iterator_values = new KeysIterator($iterator); expect(iterator_to_array($iterator_values))->toBe(['one', 'two']); expect(iterator_to_array($iterator_values))->toBe(['one', 'two']); // rewinds the iterator -}); \ No newline at end of file +}); diff --git a/tests/Iterator/MapIteratorTest.php b/tests/Iterator/MapIteratorTest.php index 9364743..8968e36 100644 --- a/tests/Iterator/MapIteratorTest.php +++ b/tests/Iterator/MapIteratorTest.php @@ -33,12 +33,12 @@ it('throws an exception when the callback arguments do not match', function () { $iterator = new \ArrayIterator(); - new MapIterator(static fn($one, $two) => '', $iterator); + new MapIterator(static fn ($one, $two) => '', $iterator); })->throws('The callback needs as many arguments as provided iterators.'); it('doesn\'t throw an exception when the callback is variadic', function () { $iterator = new \ArrayIterator([1, 2]); - $map_iterator = new MapIterator(static fn(...$values) => implode('.', $values), $iterator, clone $iterator); + $map_iterator = new MapIterator(static fn (...$values) => implode('.', $values), $iterator, clone $iterator); expect(iterator_to_array($map_iterator))->toBe(['1.1', '2.2']); }); From 719d75953c1a24835b346bc1a04b6f79502b2ec0 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Thu, 26 Aug 2021 14:36:59 +0200 Subject: [PATCH 09/13] Added tests for all exceptions on iterator functions. --- .github/workflows/php-cs-fixer.yml | 27 +++++++++++++++++++++++ .php_cs.dist.php | 4 ---- CHANGELOG.md | 7 ++++++ src/Iterator/MapIterator.php | 2 ++ tests/Functions/IteratorColumnTest.php | 1 + tests/Functions/IteratorDiffTest.php | 21 ++++++++++++++++++ tests/Functions/IteratorIntersectTest.php | 21 ++++++++++++++++++ 7 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/php-cs-fixer.yml diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml new file mode 100644 index 0000000..cd136c5 --- /dev/null +++ b/.github/workflows/php-cs-fixer.yml @@ -0,0 +1,27 @@ +name: Check & fix styling + +on: + push: + branches-ignore: + - 'master' + - 'develop' + +jobs: + php-cs-fixer: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v2 + with: + ref: ${{ github.head_ref }} + + - name: Run PHP CS Fixer + uses: docker://oskarstark/php-cs-fixer-ga + with: + args: --config=.php_cs.dist.php --allow-risky=yes + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Fix styling diff --git a/.php_cs.dist.php b/.php_cs.dist.php index 2328504..cd44517 100644 --- a/.php_cs.dist.php +++ b/.php_cs.dist.php @@ -6,7 +6,6 @@ __DIR__ . '/tests', ]) ->name('*.php') - ->notName('*.blade.php') ->ignoreDotFiles(true) ->ignoreVCS(true); @@ -16,11 +15,8 @@ 'array_syntax' => ['syntax' => 'short'], 'ordered_imports' => ['sort_algorithm' => 'alpha'], 'no_unused_imports' => true, -// 'not_operator_with_successor_space' => true, 'trailing_comma_in_multiline' => true, 'phpdoc_scalar' => true, -// 'unary_operator_spaces' => true, -// 'binary_operator_spaces' => true, 'blank_line_before_statement' => [ 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], ], diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b568be..f444d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,8 +5,15 @@ 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. +- PSR-12 code style (with php-cs-fixer workflow) + +### Fixed +- `iterator_column()` no longer returns empty values to mirror `array_column()` better. ## [1.0.0] - 2021-08-11 +### Added - Initial release [Unreleased]: https://github.com/doekenorg/iterator-functions/compare/1.0.0...HEAD diff --git a/src/Iterator/MapIterator.php b/src/Iterator/MapIterator.php index a2654b6..a2d0a06 100644 --- a/src/Iterator/MapIterator.php +++ b/src/Iterator/MapIterator.php @@ -22,9 +22,11 @@ public function __construct(callable $callback, iterable ...$iterators) { try { $function = new \ReflectionFunction(\Closure::fromCallable($callback)); + // @codeCoverageIgnoreStart } catch (\ReflectionException $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/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 index af0bd8d..7ea34f1 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -150,3 +150,24 @@ $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) { + $message = ''; + + // Would be nice if this could be Pest native + try { + $function(...$args); + } catch (\InvalidArgumentException $exception) { + $message = $exception->getMessage(); + } + + expect($message)->toBe($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 index 86990ac..e2073d2 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -167,3 +167,24 @@ $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) { + $message = ''; + + // Would be nice if this could be Pest native + try { + $function(...$args); + } catch (\InvalidArgumentException $exception) { + $message = $exception->getMessage(); + } + + expect($message)->toBe($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 () => '']], + ]); From 0f00e7bd5f78c16afb76da999593568cf4f98735 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Thu, 26 Aug 2021 23:18:39 +0200 Subject: [PATCH 10/13] Updated pest - Cleaned up exception tests with `toThrow` - Added benchmark tests for iterator_diff and iterator_map --- composer.json | 10 ++++- phpbench.json | 4 ++ tests/Assets/Functions.php | 11 +++++ tests/Assets/Model.php | 26 ++++++++++++ tests/Benchmark/IteratorDiffBench.php | 36 ++++++++++++++++ tests/Benchmark/IteratorMapBench.php | 52 +++++++++++++++++++++++ tests/Functions/IteratorDiffTest.php | 11 +---- tests/Functions/IteratorIntersectTest.php | 11 +---- tests/Functions/IteratorMapTest.php | 10 +++++ 9 files changed, 149 insertions(+), 22 deletions(-) create mode 100644 phpbench.json create mode 100644 tests/Assets/Functions.php create mode 100644 tests/Assets/Model.php create mode 100644 tests/Benchmark/IteratorDiffBench.php create mode 100644 tests/Benchmark/IteratorMapBench.php diff --git a/composer.json b/composer.json index f9a46bd..e08ab95 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,6 +18,11 @@ "src/iterator_functions.php" ] }, + "autoload-dev": { + "psr-4": { + "DoekeNorg\\IteratorFunctions\\Tests\\": "tests" + } + }, "license": "MIT", "authors": [ { 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/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/IteratorDiffTest.php b/tests/Functions/IteratorDiffTest.php index 7ea34f1..84d0400 100644 --- a/tests/Functions/IteratorDiffTest.php +++ b/tests/Functions/IteratorDiffTest.php @@ -152,16 +152,7 @@ }); test('exceptions are thrown', function (string $function, string $expected_message, array $args) { - $message = ''; - - // Would be nice if this could be Pest native - try { - $function(...$args); - } catch (\InvalidArgumentException $exception) { - $message = $exception->getMessage(); - } - - expect($message)->toBe($expected_message); + expect(static fn () => $function(...$args))->toThrow($expected_message); }) ->with([ ['iterator_diff_uassoc', 'No associative callback provided.', [$it = new ArrayIterator(), $it]], diff --git a/tests/Functions/IteratorIntersectTest.php b/tests/Functions/IteratorIntersectTest.php index e2073d2..918f8e3 100644 --- a/tests/Functions/IteratorIntersectTest.php +++ b/tests/Functions/IteratorIntersectTest.php @@ -169,16 +169,7 @@ }); test('exceptions are thrown', function (string $function, string $expected_message, array $args) { - $message = ''; - - // Would be nice if this could be Pest native - try { - $function(...$args); - } catch (\InvalidArgumentException $exception) { - $message = $exception->getMessage(); - } - - expect($message)->toBe($expected_message); + expect(static fn () => $function(...$args))->toThrow($expected_message); }) ->with([ ['iterator_intersect_uassoc', 'No associative callback provided.', [$it = new ArrayIterator(), $it]], diff --git a/tests/Functions/IteratorMapTest.php b/tests/Functions/IteratorMapTest.php index 4cc10a9..371a3d1 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 array', 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']); +}); + From da5bec0a6c8173686138f02e689ef8622634db0f Mon Sep 17 00:00:00 2001 From: doekenorg Date: Thu, 26 Aug 2021 21:19:43 +0000 Subject: [PATCH 11/13] Fix styling --- tests/Functions/IteratorMapTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Functions/IteratorMapTest.php b/tests/Functions/IteratorMapTest.php index 371a3d1..ccc95a8 100644 --- a/tests/Functions/IteratorMapTest.php +++ b/tests/Functions/IteratorMapTest.php @@ -28,4 +28,3 @@ expect($map_iterator)->toBeInstanceOf(MapIterator::class); expect(iterator_to_array($map_iterator))->toBe(['ONE', 'TWO', 'THREE']); }); - From 6b283f6984f248e143c59163e531510251cd4690 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sun, 29 Aug 2021 22:13:11 +0200 Subject: [PATCH 12/13] Added composer scripts --- composer.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e08ab95..bd9a922 100644 --- a/composer.json +++ b/composer.json @@ -29,5 +29,9 @@ "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" + } } From d4467a8112183723cd3766adcf534fc995bc1a37 Mon Sep 17 00:00:00 2001 From: Doeke Norg Date: Sun, 29 Aug 2021 22:14:00 +0200 Subject: [PATCH 13/13] fix CHANGELOG --- CHANGELOG.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35bfd53..0719b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added - Added all `_(u)diff_`, `_(u)assoc` and `_(u)intersect` methods as iterator functions. -- PSR-12 code style (with php-cs-fixer workflow) - -### Fixed -- `iterator_column()` no longer returns empty values to mirror `array_column()` better. ## [1.0.1] - 2021-08-29 ### Changed