Skip to content

Commit

Permalink
Add Equals Operation (#152)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandruGG authored Jul 19, 2021
1 parent cc52f3d commit 9808b30
Show file tree
Hide file tree
Showing 11 changed files with 395 additions and 6 deletions.
31 changes: 29 additions & 2 deletions docs/pages/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ or through the ``Collection`` object.
When used separately, operations typically return a PHP `Generator`_ or an `Iterator`_.
When used as a ``Collection`` method, operations fall into a few main categories based on the return type:

1. Operations that return a ``boolean`` or ``scalar`` value: ``Contains``, ``Count``, ``Every``, ``Falsy``, ``Has``, ``IsEmpty``, ``Match`` (or ``MatchOne``), ``Nullsy``, ``Truthy``.
1. Operations that return a ``boolean`` or ``scalar`` value: ``Contains``, ``Count``, ``Equals``, ``Every``, ``Falsy``, ``Has``, ``IsEmpty``, ``Match`` (or ``MatchOne``), ``Nullsy``, ``Truthy``.

2. Operations that return a ``Collection`` of ``Collection`` objects: ``Partition``, ``Span``.

Expand Down Expand Up @@ -334,7 +334,7 @@ asyncMapN
Asynchronously apply one or more supplied callbacks to every item of a collection and use the return value.

.. tip:: This operation is best used when multiple callbacks need to be applied. If you only want to apply
a single callback, ``asyncMap`` should be prefered as it benefits from more specific type hints.
a single callback, ``asyncMap`` should be preferred as it benefits from more specific type hints.

.. warning:: This method requires `amphp/parallel-functions <https://github.com/amphp/parallel-functions>`_ to be installed.

Expand Down Expand Up @@ -708,6 +708,31 @@ Signature: ``Collection::duplicate(): Collection;``
->distinct()
->normalize() // [0 => 'a', 1 => 'c']
equals
~~~~~~

Compare two collections for equality. Collections are considered *equal* if:

* they have the same number of elements;
* they contain the same elements, regardless of the order they appear in or their keys.

Elements will be compared using strict equality (``===``).

.. tip:: This operation enables comparing ``Collection`` objects in PHPUnit tests using
the dedicated `assertObjectEquals`_ assertion.

.. warning:: Because this operation *needs to traverse both collections* to determine if
the same elements are contained within them, a performance cost is incurred. Even though
the operation will stop as soon as it encounters an element of one collection that cannot
be found in the other, it is not recommended to use this for potentially large collections.

Interface: `Equalsable`_

Signature: ``Collection::equals(Collection $other): bool;``

.. literalinclude:: code/operations/equals.php
:language: php

every
~~~~~

Expand Down Expand Up @@ -2433,6 +2458,7 @@ Signature: ``Collection::zip(iterable ...$iterables): Collection;``
.. _DropWhileable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/DropWhileable.php
.. _Dumpable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Dumpable.php
.. _Duplicateable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Duplicateable.php
.. _Equalsable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Equalsable.php
.. _Everyable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Everyable.php
.. _Explodeable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Explodeable.php
.. _Falsyable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Falsyable.php
Expand Down Expand Up @@ -2520,6 +2546,7 @@ Signature: ``Collection::zip(iterable ...$iterables): Collection;``
.. _Zipable: https://github.com/loophp/collection/blob/master/src/Contract/Operation/Zipable.php

.. _array_flip(): https://php.net/array_flip
.. _assertObjectEquals: https://phpunit.readthedocs.io/en/9.5/assertions.html#assertobjectequals
.. _Countable: https://www.php.net/manual/en/class.countable.php
.. _Criteria: https://www.doctrine-project.org/projects/doctrine-collections/en/1.6/index.html#matching
.. _Doctrine Collections: https://github.com/doctrine/collections
Expand Down
41 changes: 41 additions & 0 deletions docs/pages/code/operations/equals.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

/**
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

declare(strict_types=1);

namespace App;

use loophp\collection\Collection;

include __DIR__ . '/../../../../vendor/autoload.php';

Collection::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([1, 2, 3])); // true

Collection::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([3, 1, 2])); // true

Collection::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([1, 2])); // false

Collection::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([1, 2, 4])); // false

Collection::fromIterable(['foo' => 'f'])
->equals(Collection::fromIterable(['foo' => 'f'])); // true

Collection::fromIterable(['foo' => 'f', 'bar' => 'b'])
->equals(Collection::fromIterable(['foo' => 'f', 'baz' => 'b'])); // true

$a = (object) ['id' => 'a'];
$a2 = (object) ['id' => 'a'];

Collection::fromIterable([$a])
->equals(Collection::fromIterable([$a])); // true

Collection::fromIterable([$a])
->equals(Collection::fromIterable([$a2])); // false
116 changes: 116 additions & 0 deletions spec/loophp/collection/CollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@
use function gettype;
use const INF;
use const PHP_EOL;
use const PHP_VERSION_ID;

class CollectionSpec extends ObjectBehavior
{
private const PHP_8 = 80_000;

public function it_can_all(): void
{
$this::fromIterable([1, 2, 3])
Expand Down Expand Up @@ -785,6 +788,28 @@ public function it_can_diff(): void
$this::fromIterable(range(1, 5))
->diff()
->shouldIterateAs(range(1, 5));

$this::fromIterable(range(1, 5))
->diff(...Collection::fromIterable(range(2, 5)))
->shouldIterateAs([0 => 1]);

$this::fromIterable(['foo' => 'f', 'bar' => 'b'])
->diff(...Collection::fromIterable(['f']))
->shouldIterateAs(['bar' => 'b']);

$this::fromIterable(['foo' => 'f', 'bar' => 'b'])
->diff('F', 'b')
->shouldIterateAs(['foo' => 'f']);

if (PHP_VERSION_ID >= self::PHP_8) {
$this::fromIterable(['foo' => 'f', 'bar' => 'b'])
->diff(...Collection::fromIterable(['foo' => 'f']))
->shouldIterateAs(['bar' => 'b']);

$this::fromIterable(['foo' => 'f', 'bar' => 'b'])
->diff(...['foo' => 'F', 'bar' => 'b'])
->shouldIterateAs(['foo' => 'f']);
}
}

public function it_can_diffKeys(): void
Expand Down Expand Up @@ -1022,6 +1047,97 @@ public function it_can_duplicate(): void
->shouldIterateAs($result());
}

public function it_can_equals(): void
{
$a = (object) ['id' => 'a'];
$a2 = (object) ['id' => 'a'];
$b = (object) ['id' => 'b'];

// empty variations
$this::empty()
->equals(Collection::empty())
->shouldBe(true);

$this::empty()
->equals(Collection::fromIterable([1]))
->shouldBe(false);

$this::fromIterable([1])
->equals(Collection::empty())
->shouldBe(false);

// same elements, same order
$this::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([1, 2, 3]))
->shouldBe(true);

$this::fromIterable([$a, $b])
->equals(Collection::fromIterable([$a, $b]))
->shouldBe(true);

// same elements, different order
$this::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([3, 1, 2]))
->shouldBe(true);

$this::fromIterable([$a, $b])
->equals(Collection::fromIterable([$b, $a]))
->shouldBe(true);

// same lengths, with one element different
$this::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([1, 2, 4]))
->shouldBe(false);

// different lengths, missing elements
$this::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([1, 2]))
->shouldBe(false);

// different lengths, extra elements in first
$this::fromIterable([1, 2, 3, 4])
->equals(Collection::fromIterable([1, 2, 3]))
->shouldBe(false);

// different lengths, extra elements in second
$this::fromIterable([1, 2, 3])
->equals(Collection::fromIterable([1, 2, 3, 4]))
->shouldBe(false);

// objects, different instances and contents
$this::fromIterable([$a])
->equals(Collection::fromIterable([$b]))
->shouldBe(false);

// objects, different instances but same contents
$this::fromIterable([$a])
->equals(Collection::fromIterable([$a2]))
->shouldBe(false);

// only in PHP 8 due to unpacking iterable with string keys
if (PHP_VERSION_ID >= 80000) {
$this::fromIterable(['foo' => 'f', 'bar' => 'b'])
->equals(Collection::fromIterable(['foo' => 'f', 'bar' => 'b']))
->shouldBe(true);

$this::fromIterable(['foo' => 'f', 'bar' => 'b'])
->equals(Collection::fromIterable(['bar' => 'b', 'foo' => 'f']))
->shouldBe(true);

$this::fromIterable(['foo' => 'f', 'bar' => 'b'])
->equals(Collection::fromIterable(['bar' => 'b']))
->shouldBe(false);

$this::fromIterable(['foo' => 'f'])
->equals(Collection::fromIterable(['bar' => 'b']))
->shouldBe(false);

$this::fromIterable(['foo' => 'f'])
->equals(Collection::fromIterable(['foo' => 'f', 'bar' => 'b']))
->shouldBe(false);
}
}

public function it_can_every(): void
{
$input = range(0, 10);
Expand Down
8 changes: 7 additions & 1 deletion src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
use loophp\collection\Operation\DropWhile;
use loophp\collection\Operation\Dump;
use loophp\collection\Operation\Duplicate;
use loophp\collection\Operation\Equals;
use loophp\collection\Operation\Every;
use loophp\collection\Operation\Explode;
use loophp\collection\Operation\Falsy;
Expand Down Expand Up @@ -343,6 +344,11 @@ public static function empty(): CollectionInterface
return self::fromIterable($emptyArray);
}

public function equals(CollectionInterface $other): bool
{
return (new self(Equals::of()($other->getIterator()), [$this->getIterator()]))->getIterator()->current();
}

public function every(callable ...$callbacks): bool
{
return (new self(Every::of()(static fn (): bool => false)(...$callbacks), [$this->getIterator()]))->getIterator()->current();
Expand Down Expand Up @@ -572,7 +578,7 @@ public function intersperse($element, int $every = 1, int $startAt = 0): Collect

public function isEmpty(): bool
{
return IsEmpty::of()($this->getIterator());
return (new self(IsEmpty::of(), [$this->getIterator()]))->getIterator()->current();
}

/**
Expand Down
3 changes: 3 additions & 0 deletions src/Contract/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use loophp\collection\Contract\Operation\DropWhileable;
use loophp\collection\Contract\Operation\Dumpable;
use loophp\collection\Contract\Operation\Duplicateable;
use loophp\collection\Contract\Operation\Equalsable;
use loophp\collection\Contract\Operation\Everyable;
use loophp\collection\Contract\Operation\Explodeable;
use loophp\collection\Contract\Operation\Falsyable;
Expand Down Expand Up @@ -156,6 +157,7 @@
* @template-extends DropWhileable<TKey, T>
* @template-extends Dumpable<TKey, T>
* @template-extends Duplicateable<TKey, T>
* @template-extends Equalsable<TKey, T>
* @template-extends Everyable<TKey, T>
* @template-extends Explodeable<TKey, T>
* @template-extends Falsyable<TKey, T>
Expand Down Expand Up @@ -269,6 +271,7 @@ interface Collection extends
DropWhileable,
Dumpable,
Duplicateable,
Equalsable,
Everyable,
Explodeable,
Falsyable,
Expand Down
2 changes: 1 addition & 1 deletion src/Contract/Operation/Diffable.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
interface Diffable
{
/**
* @param mixed ...$values
* @param T ...$values
*
* @return Collection<TKey, T>
*/
Expand Down
26 changes: 26 additions & 0 deletions src/Contract/Operation/Equalsable.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

/**
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

declare(strict_types=1);

namespace loophp\collection\Contract\Operation;

use loophp\collection\Contract\Collection;

/**
* @template TKey
* @template T
*/
interface Equalsable
{
/**
* Check if the collection equals another iterable.
*
* @param Collection<TKey, T> $other
*/
public function equals(Collection $other): bool;
}
Loading

0 comments on commit 9808b30

Please sign in to comment.