diff --git a/src/Seq.php b/src/Seq.php index ef7d5e2..6c5b861 100644 --- a/src/Seq.php +++ b/src/Seq.php @@ -129,4 +129,31 @@ public static function map(iterable $traversable, callable $callback): Generator yield $key => $callback($value); } } + + /** + * Yield every unique value of the given traversable with its original key + * + * @param iterable $traversable + * @param bool $caseSensitive + * + * @return Generator + */ + public static function unique(iterable $traversable, bool $caseSensitive = true): Generator + { + $seen = []; + foreach ($traversable as $key => $value) { + if (is_string($value)) { + $needle = $caseSensitive ? $value : strtolower($value); + } elseif (is_object($value)) { + $needle = spl_object_hash($value); + } else { + $needle = $value; + } + + if (! array_key_exists($needle, $seen)) { + $seen[$needle] = true; + yield $key => $value; + } + } + } } diff --git a/tests/SeqTest.php b/tests/SeqTest.php index a66da9d..7c46840 100644 --- a/tests/SeqTest.php +++ b/tests/SeqTest.php @@ -4,6 +4,7 @@ use ArrayIterator; use ipl\Stdlib\Seq; +use stdClass; class SeqTest extends TestCase { @@ -214,4 +215,82 @@ public function testMapWithKeyedGenerators() $result ); } + + public function testUniqueWithArray() + { + $object1 = new stdClass(); + $object2 = new stdClass(); + $arr = [1, 2, 2, 3, '3', $object1, $object1, $object2]; + $result = iterator_to_array(Seq::unique($arr)); + + $this->assertSame( + [0 => 1, 1 => 2, 3 => 3, 5 => $object1, 7 => $object2], + $result + ); + } + + public function testUniqueWithKeyedArray() + { + $object1 = new stdClass(); + $object2 = new stdClass(); + $arr = ['a' => 1, 'b' => 2, 0 => 2, -1 => 3, 1 => '3', 2 => $object1, 3 => $object1, 4 => $object2]; + $result = iterator_to_array(Seq::unique($arr)); + + $this->assertSame( + ['a' => 1, 'b' => 2, -1 => 3, 2 => $object1, 4 => $object2], + $result + ); + } + + public function testUniqueWithGenerator() + { + $object1 = new stdClass(); + $object2 = new stdClass(); + $generator = function () use ($object1, $object2) { + yield from [1, 2, 2, 3, '3', $object1, $object1, $object2]; + }; + $result = iterator_to_array(Seq::unique($generator())); + + $this->assertSame( + [0 => 1, 1 => 2, 3 => 3, 5 => $object1, 7 => $object2], + $result + ); + } + + public function testUniqueWithKeyedGenerator() + { + $object1 = new stdClass(); + $object2 = new stdClass(); + $generator = function () use ($object1, $object2) { + yield from ['a' => 1, 'b' => 2, 0 => 2, -1 => 3, 1 => '3', 2 => $object1, 3 => $object1, 4 => $object2]; + }; + $result = iterator_to_array(Seq::unique($generator())); + + $this->assertSame( + ['a' => 1, 'b' => 2, -1 => 3, 2 => $object1, 4 => $object2], + $result + ); + } + + public function testUniqueWithStringsIsCaseSensitiveByDefault() + { + $arr = ['foo', 'bar', 'FOO', 'BAR', 'foo']; + $result = iterator_to_array(Seq::unique($arr)); + + $this->assertSame( + [0 => 'foo', 1 => 'bar', 2 => 'FOO', 3 => 'BAR'], + $result + ); + } + + public function testUniqueWithStringsCaseInsensitive() + { + $arr = ['foo', 'bar', 'FOO', 'BAR', 'foo']; + $result = iterator_to_array(Seq::unique($arr, false)); + + $this->assertSame( + [0 => 'foo', 1 => 'bar'], + $result + ); + } }