packages/ztd-query-core/src/Shadow/Row/RowMultiset.php

1<?php
2
3declare(strict_types=1);
4
5namespace ZtdQuery\Shadow\Row;
6
7use ZtdQuery\Schema\TableDefinition;
8
9/**
10 * Rows counted with their repeats: what one set of rows has that another does not.
11 *
12 * A table may hold the same row twice, so comparing two states of it cannot be
13 * done with set arithmetic — a row that appears three times before and once
14 * after has been removed twice. Every row here is paired off against at most
15 * one row on the other side, and what stays unpaired is the difference.
16 *
17 * Column order is not part of a row's identity: the same columns carrying the
18 * same values are the same row however the reader happened to order them.
19 *
20 * @phpstan-import-type Row from TableDefinition
21 */
22final class RowMultiset
23{
24    /**
25     * @param RowMatch $match Decides when two rows are the same row
26     */
27    public function __construct(private readonly RowMatch $match = new RowMatch())
28    {
29    }
30
31    /**
32     * Answers the rows on the left that nothing on the right pairs with.
33     *
34     * @param array<int, Row> $left Rows to account for
35     * @param array<int, Row> $right Rows they are paired off against
36     *
37     * @return list<Row> The unpaired rows, in the order the left held them
38     */
39    public function difference(array $left, array $right): array
40    {
41        $remaining = $right;
42        $difference = [];
43        foreach ($left as $row) {
44            $paired = null;
45            foreach ($remaining as $index => $candidate) {
46                if ($this->match->sameRow($row, $candidate)) {
47                    $paired = $index;
48                    break;
49                }
50            }
51            if ($paired === null) {
52                $difference[] = $row;
53                continue;
54            }
55            unset($remaining[$paired]);
56        }
57
58        return $difference;
59    }
60}
61