Passed
Push — master ( 28ae8d...ea62ee )
by Ondřej
02:07
created

RelationMacros::colImpl()   B

Complexity

Conditions 8
Paths 7

Size

Total Lines 25
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 19
nc 7
nop 4
dl 0
loc 25
rs 8.4444
c 0
b 0
f 0
1
<?php
2
declare(strict_types=1);
3
namespace Ivory\Relation;
4
5
use Ivory\Data\Map\ArrayRelationMap;
6
use Ivory\Data\Map\ArrayValueMap;
7
use Ivory\Data\Map\IRelationMap;
8
use Ivory\Data\Map\ITupleMap;
9
use Ivory\Data\Map\IValueMap;
10
use Ivory\Data\Map\IWritableTupleMap;
11
use Ivory\Data\Map\IWritableValueMap;
12
use Ivory\Data\Set\DictionarySet;
13
use Ivory\Data\Set\ISet;
14
use Ivory\Exception\AmbiguousException;
15
use Ivory\Exception\UndefinedColumnException;
16
use Ivory\Value\Alg\ITupleEvaluator;
17
use Ivory\Data\Map\ArrayTupleMap;
18
19
/**
20
 * Standard implementations of IRelation operations which are defined solely in terms of other operations.
21
 *
22
 * Note the trait may only be used by a class implementing {@link IRelation}.
23
 */
24
trait RelationMacros
25
{
26
    abstract public function project($columns): IRelation;
27
28
    abstract public function tuple(int $offset = 0): ITuple;
29
30
    public function col($offsetOrNameOrEvaluator): IColumn
31
    {
32
        $thisRel = $this;
33
        assert($thisRel instanceof IRelation);
34
        return $this->colImpl($offsetOrNameOrEvaluator, $this->columns, $this->colNameMap, $thisRel);
35
    }
36
37
38
    protected static function computeColNameMap(IRelation $relation): array
39
    {
40
        $result = [];
41
        foreach ($relation->getColumns() as $i => $col) {
42
            $name = $col->getName();
43
            if ($name !== null && !isset($result[$name])) {
44
                $result[$name] = $i;
45
            }
46
        }
47
        return $result;
48
    }
49
50
    public function toSet($colOffsetOrNameOrEvaluator, ?ISet $set = null): ISet
51
    {
52
        if ($set === null) {
53
            $set = new DictionarySet();
54
        }
55
56
        foreach ($this->col($colOffsetOrNameOrEvaluator) as $value) {
57
            $set->add($value);
58
        }
59
60
        return $set;
61
    }
62
63
    final public function extend($extraColumns): IRelation
64
    {
65
        if ($extraColumns instanceof \Traversable) {
66
            $extraColumns = iterator_to_array($extraColumns);
67
        }
68
69
        return $this->project(array_merge(['*'], $extraColumns));
70
    }
71
72
    final public function toArray(): array
73
    {
74
        $result = [];
75
        foreach ($this as $tuple) {
76
            assert($tuple instanceof ITuple);
77
            $result[] = $tuple->toMap();
78
        }
79
        return $result;
80
    }
81
82
    final public function value($colOffsetOrNameOrEvaluator = 0, int $tupleOffset = 0)
83
    {
84
        return $this->tuple($tupleOffset)->value($colOffsetOrNameOrEvaluator);
85
    }
86
87
    /**
88
     * @param $offsetOrNameOrEvaluator
89
     * @param $columns
90
     * @param $colNameMap
91
     * @param IRelation $relation
92
     * @return IColumn
93
     * @internal
94
     */
95
    final protected function colImpl($offsetOrNameOrEvaluator, $columns, $colNameMap, IRelation $relation): IColumn
96
    {
97
        if (is_scalar($offsetOrNameOrEvaluator)) {
98
            if (filter_var($offsetOrNameOrEvaluator, FILTER_VALIDATE_INT) !== false) {
99
                if (isset($columns[$offsetOrNameOrEvaluator])) {
100
                    return $columns[$offsetOrNameOrEvaluator];
101
                } else {
102
                    throw new UndefinedColumnException("No column at offset $offsetOrNameOrEvaluator");
103
                }
104
            } else {
105
                if (!isset($colNameMap[$offsetOrNameOrEvaluator])) {
106
                    throw new UndefinedColumnException("No column named $offsetOrNameOrEvaluator");
107
                } elseif ($colNameMap[$offsetOrNameOrEvaluator] === Tuple::AMBIGUOUS_COL) {
108
                    throw new AmbiguousException("Multiple columns named $offsetOrNameOrEvaluator");
109
                } else {
110
                    return $columns[$colNameMap[$offsetOrNameOrEvaluator]];
111
                }
112
            }
113
        } elseif (
114
            $offsetOrNameOrEvaluator instanceof ITupleEvaluator ||
115
            $offsetOrNameOrEvaluator instanceof \Closure
116
        ) {
117
            return new Column($relation, $offsetOrNameOrEvaluator, null, null);
118
        } else {
119
            throw new \InvalidArgumentException('$offsetOrNameOrEvaluator');
120
        }
121
    }
122
123
    public function assoc($col1, $col2, ...$moreCols): IValueMap
124
    {
125
        return $this->assocImpl(
126
            function () {
127
                // FIXME: Depending on the data type of the keys, either use an array-based implementation, or an object
128
                //        hashing implementation.
129
                return new ArrayValueMap();
130
            },
131
            true, array_merge([$col1, $col2], $moreCols)
132
        );
133
    }
134
135
    public function map($mappingCol, ...$moreMappingCols): ITupleMap
136
    {
137
        return $this->assocImpl(
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->assocImpl(...ol), $moreMappingCols)) returns the type Ivory\Data\Map\IWritableValueMap which is incompatible with the type-hinted return Ivory\Data\Map\ITupleMap.
Loading history...
138
            function () {
139
                // FIXME: Depending on the data type of the keys, either use an array-based implementation, or an object
140
                //        hashing implementation.
141
                return new ArrayTupleMap();
142
            },
143
            false, array_merge([$mappingCol], $moreMappingCols)
144
        );
145
    }
146
147
    public function multimap($mappingCol, ...$moreMappingCols): IRelationMap
148
    {
149
        $mappingCols = array_merge([$mappingCol], $moreMappingCols);
150
        $multiDimKeyCols = array_slice($mappingCols, 0, -1);
151
        $lastKeyCol = $mappingCols[count($mappingCols) - 1];
152
153
        // FIXME: Depending on the data type of the keys, either use an array-based implementation, or an object hashing
154
        //        implementation.
155
        $map = new ArrayRelationMap();
156
        $emptyMap = new ArrayRelationMap();
157
        foreach ($this as $tupleOffset => $tuple) {
158
            assert($tuple instanceof ITuple);
159
            $m = $map;
160
            foreach ($multiDimKeyCols as $col) {
161
                $key = $tuple->value($col);
162
                $added = $m->putIfNotExists($key, $emptyMap);
163
                if ($added) {
164
                    $emptyMap = new ArrayRelationMap(); // prepare a new copy for the following iterations
165
                }
166
                $m = $m[$key];
167
            }
168
169
            $key = $tuple->value($lastKeyCol);
170
            $rel = $m->maybe($key);
171
            if ($rel === null) {
172
                $thisRel = $this;
173
                assert($thisRel instanceof IRelation);
174
                $rel = new CherryPickedRelation($thisRel, []);
175
                $m->put($key, $rel);
176
            }
177
            $rel->cherryPick($tupleOffset);
178
        }
179
180
        return $map;
181
    }
182
183
    /**
184
     * @param \Closure $emptyMapFactory closure creating an empty <tt>IWritableTupleMap</tt> or
185
     *                                    <tt>IWritableValueMap</tt> for storing the data
186
     * @param bool $lastForValues whether the last of <tt>$cols</tt> shall be used for mapped values
187
     * @param array $cols
188
     * @return IWritableValueMap|ITupleMap
189
     */
190
    private function assocImpl(\Closure $emptyMapFactory, bool $lastForValues, array $cols)
191
    {
192
        $multiDimKeyCols = array_slice($cols, 0, -1 - (int)$lastForValues);
193
        $lastKeyCol = $cols[count($cols) - 1 - (int)$lastForValues];
194
        $valueCol = ($lastForValues ? $cols[count($cols) - 1] : null);
195
196
        $map = $emptyMapFactory();
197
        assert($map instanceof IWritableTupleMap || $map instanceof IWritableValueMap);
198
        $emptyMap = $emptyMapFactory();
199
        foreach ($this as $tuple) {
200
            assert($tuple instanceof ITuple);
201
            $m = $map;
202
            foreach ($multiDimKeyCols as $col) {
203
                $key = $tuple->value($col);
204
                $added = $m->putIfNotExists($key, $emptyMap);
205
                if ($added) {
206
                    $emptyMap = $emptyMapFactory(); // prepare a new copy for the following iterations
207
                }
208
                $m = $m[$key];
209
            }
210
211
            $key = $tuple->value($lastKeyCol);
212
            $val = ($lastForValues ? $tuple->value($valueCol) : $tuple);
213
            $added = $m->putIfNotExists($key, $val);
214
215
            if (!$added) {
216
                $keys = [];
217
                foreach ($multiDimKeyCols as $col) {
218
                    $keys[] = $tuple->value($col);
219
                }
220
                $keys[] = $tuple->value($lastKeyCol);
221
                $keyDesc = implode(', ', $keys);
222
                trigger_error(
223
                    "Duplicate entry under key ($keyDesc). Skipping. Consider using multimap() instead.",
224
                    E_USER_WARNING
225
                );
226
            }
227
        }
228
229
        return $map;
230
    }
231
232
    //region \IteratorAggregate
233
234
    public function getIterator()
235
    {
236
        $thisRel = $this;
237
        assert($thisRel instanceof IRelation);
238
        return new RelationSeekableIterator($thisRel);
239
    }
240
241
    //endregion
242
}
243