-1

I have an array of arrays of objects, and I'm trying to get the intersection of objects found in all of the inner arrays. My first attempt was to use the spread operator on the outer array and pass that to array_intersect:

array_intersect(...array_values($test))

The function array_intersect, however, uses a string-based comparison string representation:

Two elements are considered equal if and only if (string) $elem1 === (string) $elem2. In words: when the string representation is the same.

I can easily implement __toString on most of my objects however one is already using this for another purpose and it isn't guaranteed to be unique. If I have to bite the bullet and refactor, I will, but I'm just seeing if there's another way to solve this cleanly.

The keys in the array do not matter, and both outer and inner arrays are not bounded.

(This is an aggregated search result from a bunch of components that is being AND'd together if anyone is wondering).

The basic gist is this:

class sample
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

    // Comment this function out to see the error
    public function __toString()
    {
        return spl_object_hash($this);
    }
}

$obj1 = new sample('1');
$obj2 = new sample('2');
$obj3 = new sample('3');
$obj4 = new sample('4');
$obj5 = new sample('5');

$test = [
    'a' => [
        $obj1,
        $obj2,
        $obj5,
    ],
    'b' => [
        $obj2,
        $obj3,
        $obj5,
    ],
    'c' => [
        $obj2,
        $obj3,
        $obj5,
    ],
    'd' => [
        $obj2,
        $obj4,
        $obj5,
    ],
];

print_r(array_intersect(...array_values($test)));

Running that will produce exactly what I want:


Array
(
    [1] => sample Object
        (
            [id] => 2
        )

    [2] => sample Object
        (
            [id] => 5
        )

)

However, if you comment out the __toString() method you'll see the exception Object of class sample could not be converted to string

Chris Haas
  • 53,986
  • 12
  • 141
  • 274

2 Answers2

2

Okay, I spent so long writing that that my brain was able to shift thinking and I came up with an answer on my own. Not a elegant but pretty clean still, and I don't need to refactor.

$final = array_shift($test);
while (count($test)) {
    $to_test = array_shift($test);
    $final = array_uintersect($final, $to_test, static function ($a, $b) {
        return spl_object_hash($a) <=> spl_object_hash($b);
    });
}

This just uses array_uintersect which allows a custom comparison function and we loop over each array one at a time and compute the intersection

Chris Haas
  • 53,986
  • 12
  • 141
  • 274
  • Spaceship operator may be enough: `print_r( call_user_func_array( 'array_uintersect', array_merge($test, [function($a, $b) { return $a <=> $b; }]) ) );` – Progrock Jun 26 '20 at 23:13
  • Thanks @Progrock, a quick test shows that using the spaceship without spl_object_hash does indeed work! – Chris Haas Jun 28 '20 at 16:39
1

Well, you can also implement your own comparator function.

Take a look.

<?php declare(strict_types=1);

final class SampleComparator
{
    public static function compare(array $samples): array
    {
        if (!$firstElement = array_shift($samples)) {
            // If array is empty
            return [];
        }

        return self::compareWithSampleElements($firstElement, $samples);
    }

    private static function compareWithSampleElements(
        array $headSamples,
        array $tailListSamples
    ): array {
        $result = [];

        /** @var Sample $headSample */
        foreach ($headSamples as $headSample) {
            $result = self::iterateHeadSamples($tailListSamples, $headSample, $result);
        }

        return $result;
    }

    private static function iterateHeadSamples(
        array $tailListSamples,
        Sample $headSample,
        array $result
    ): array {
        /** @var array $tailSamples */
        foreach ($tailListSamples as $tailSamples) {
            $result = self::iterateTailSamples($tailSamples, $headSample, $result);
        }

        return $result;
    }

    private static function iterateTailSamples(
        array $tailSamples,
        Sample $headSample,
        array $result
    ): array {
        foreach ($tailSamples as $tailSample) {
            if ($headSample === $tailSample) {
                $result[$headSample->id] = $headSample;
                continue;
            }
        }

        return $result;
    }
}

Usage:

$intersection = SampleComparator::compare($test);
print_r($intersection);
----------
*OUTPUT*

Array
(
    [2] => Sample Object
        (
            [id] => 2
        )

    [5] => Sample Object
        (
            [id] => 5
        )
)

And the tests, of course.

<?php declare(strict_types=1);

final class SampleComparatorTest extends TestCase
{
    /**
     * @test
     * @dataProvider sampleProvider
     */
    public function sampleComparator(array $expected, array $actual): void
    {
        self::assertSame($expected, SampleComparator::compare($actual));
    }

    public function sampleProvider(): array
    {
        $obj1 = new Sample('1');
        $obj2 = new Sample('2');
        $obj3 = new Sample('3');
        $obj4 = new Sample('4');
        $obj5 = new Sample('5');

        return [
            'no values at all' => [
                [],
                []
            ],
            'no tail values' => [
                [],
                [
                    [$obj1, $obj2]
                ]
            ],
            'no same values' => [
                [],
                [
                    [$obj1, $obj2],
                    [$obj3, $obj4, $obj5]
                ]
            ],
            'repeat values' => [
                [1 => $obj1],
                [
                    [$obj1, $obj1],
                    [$obj1, $obj1, $obj1]
                ]
            ],
            'only 2 and 5' => [
                [2 => $obj2, 5 => $obj5],
                [
                    [$obj1, $obj2, $obj5],
                    [$obj2, $obj3, $obj5],
                    [$obj2, $obj3, $obj4],
                ]
            ],
        ];
    }
}
JesusValera
  • 629
  • 6
  • 14