1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Bdf\Collection; |
4
|
|
|
|
5
|
|
|
use Bdf\Collection\Stream\ArrayStream; |
6
|
|
|
use Bdf\Collection\Stream\StreamInterface; |
7
|
|
|
use Bdf\Collection\Util\Hash; |
8
|
|
|
use Bdf\Collection\Util\Optional; |
9
|
|
|
use Bdf\Collection\Util\OptionalInterface; |
10
|
|
|
use function array_values; |
11
|
|
|
use function count; |
12
|
|
|
|
13
|
|
|
/** |
14
|
|
|
* Set implementation using an hash table |
15
|
|
|
* |
16
|
|
|
* This set implementation handle scalar value, objects and arrays |
17
|
|
|
* Two elements are considered as equals when there hash values are equals |
18
|
|
|
* |
19
|
|
|
* By default the used hash function is Hash::compute(), but you can define a custom hash function in constructor |
20
|
|
|
* |
21
|
|
|
* /!\ Because the hash function is used for comparisons, the $strict parameter in methods remove() and contains() is not used |
22
|
|
|
* |
23
|
|
|
* <code> |
24
|
|
|
* $set = new HashSet(); |
25
|
|
|
* |
26
|
|
|
* $set->add(new Person('Mickey', 'Mouse')); // true |
27
|
|
|
* $set->add(new Person('Mickey', 'Mouse')); // false : Mickey is already added |
28
|
|
|
* $set->contains(new Person('Mickey', 'Mouse')); // true |
29
|
|
|
* |
30
|
|
|
* $set->add(new Person('John', 'Doe')); // true |
31
|
|
|
* $set->add(new Person('John', 'Smith')); // true |
32
|
|
|
* |
33
|
|
|
* $setWithCustomHash = new HashSet(function ($person) { return $person->firstName(); }); // Compare only the first name |
34
|
|
|
* |
35
|
|
|
* $setWithCustomHash->add(new Person('John', 'Doe')); |
36
|
|
|
* |
37
|
|
|
* $setWithCustomHash->contains(new Person('John', 'Smith')); // true : The hash function consider only the first name |
38
|
|
|
* $setWithCustomHash->add(new Person('John', 'Smith')); // false : Considered as already added ! |
39
|
|
|
* </code> |
40
|
|
|
* |
41
|
|
|
* /!\ The default hash function distinguish by type and value, so int(123) is not equals with string('123') |
42
|
|
|
* If you want to compare without consider the type, you must define a custom hash function, for example : `function ($e) { return (string) $e; }` |
43
|
|
|
* |
44
|
|
|
* Ex: |
45
|
|
|
* <code> |
46
|
|
|
* $set = new HashSet(); |
47
|
|
|
* $set->add(123); |
48
|
|
|
* $set->contains(123); // true |
49
|
|
|
* $set->contains('123'); // false |
50
|
|
|
* <code> |
51
|
|
|
* |
52
|
|
|
* (i) About performance : |
53
|
|
|
* - add() + toArray() has linear complexity (adding 10x more elements takes 10x more time), whereas array_unique() has O(n.log(n)) complexity |
54
|
|
|
* - contains() is about 3 times faster than array_search() on 10k elements |
55
|
|
|
* - For 10k elements HashSet add() + toArray() vs array_unique : |
56
|
|
|
* - With integers, HashSet about 2 times slower |
57
|
|
|
* - With objects without custom hash, HashSet is about 30% slower |
58
|
|
|
* - With objects with custom hash, HashSet has same performances |
59
|
|
|
* |
60
|
|
|
* @see Hash::compute() The default hash function |
61
|
|
|
* |
62
|
|
|
* @template T |
63
|
|
|
* @implements SetInterface<T> |
64
|
|
|
* |
65
|
|
|
* @psalm-consistent-constructor |
66
|
|
|
*/ |
67
|
|
|
class HashSet implements SetInterface |
68
|
|
|
{ |
69
|
|
|
use CollectionTrait; |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @var array |
73
|
|
|
*/ |
74
|
|
|
private $data = []; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* @var callable |
78
|
|
|
*/ |
79
|
|
|
private $hashFunction; |
80
|
|
|
|
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* HashSet constructor. |
84
|
|
|
* |
85
|
|
|
* @param callable $hashFunction The the hash function. Takes as parameter the element to hash, and should return a string |
86
|
|
|
*/ |
87
|
49 |
|
public function __construct(callable $hashFunction = null) |
88
|
|
|
{ |
89
|
49 |
|
$this->hashFunction = $hashFunction ?: [Hash::class, 'compute']; |
90
|
49 |
|
} |
91
|
|
|
|
92
|
|
|
/** |
93
|
|
|
* {@inheritdoc} |
94
|
|
|
*/ |
95
|
49 |
|
public function add($element): bool |
96
|
|
|
{ |
97
|
49 |
|
$index = ($this->hashFunction)($element); |
98
|
|
|
|
99
|
49 |
|
if (isset($this->data[$index])) { |
100
|
30 |
|
return false; |
101
|
|
|
} |
102
|
|
|
|
103
|
49 |
|
$this->data[$index] = $element; |
104
|
49 |
|
return true; |
105
|
|
|
} |
106
|
|
|
|
107
|
|
|
/** |
108
|
|
|
* {@inheritdoc} |
109
|
|
|
*/ |
110
|
7 |
|
public function contains($element, bool $strict = false): bool |
111
|
|
|
{ |
112
|
7 |
|
$key = ($this->hashFunction)($element); |
113
|
|
|
|
114
|
7 |
|
if (!isset($this->data[$key])) { |
115
|
5 |
|
return false; |
116
|
|
|
} |
117
|
|
|
|
118
|
7 |
|
return !$strict || $this->data[$key] === $element; |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
/** |
122
|
|
|
* {@inheritdoc} |
123
|
|
|
*/ |
124
|
1 |
|
public function lookup($element): OptionalInterface |
125
|
|
|
{ |
126
|
1 |
|
$index = ($this->hashFunction)($element); |
127
|
|
|
|
128
|
1 |
|
if (!isset($this->data[$index])) { |
129
|
1 |
|
return Optional::empty(); |
130
|
|
|
} |
131
|
|
|
|
132
|
1 |
|
return Optional::of($this->data[$index]); |
133
|
|
|
} |
134
|
|
|
|
135
|
|
|
/** |
136
|
|
|
* {@inheritdoc} |
137
|
|
|
*/ |
138
|
3 |
|
public function remove($element, bool $strict = false): bool |
139
|
|
|
{ |
140
|
3 |
|
$index = ($this->hashFunction)($element); |
141
|
|
|
|
142
|
3 |
|
if (!isset($this->data[$index])) { |
143
|
2 |
|
return false; |
144
|
|
|
} |
145
|
|
|
|
146
|
3 |
|
if (!$strict || $this->data[$index] === $element) { |
147
|
3 |
|
unset($this->data[$index]); |
148
|
3 |
|
return true; |
149
|
|
|
} |
150
|
|
|
|
151
|
|
|
return false; |
152
|
|
|
} |
153
|
|
|
|
154
|
|
|
/** |
155
|
|
|
* {@inheritdoc} |
156
|
|
|
*/ |
157
|
34 |
|
public function clear(): void |
158
|
|
|
{ |
159
|
34 |
|
$this->data = []; |
160
|
34 |
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* {@inheritdoc} |
164
|
|
|
*/ |
165
|
1 |
|
public function empty(): bool |
166
|
|
|
{ |
167
|
1 |
|
return empty($this->data); |
168
|
|
|
} |
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* {@inheritdoc} |
172
|
|
|
*/ |
173
|
1 |
|
public function forEach(callable $consumer): void |
174
|
|
|
{ |
175
|
1 |
|
foreach ($this->data as $value) { |
176
|
1 |
|
$consumer($value); |
177
|
|
|
} |
178
|
1 |
|
} |
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* {@inheritdoc} |
182
|
|
|
*/ |
183
|
9 |
|
public function toArray(): array |
184
|
|
|
{ |
185
|
9 |
|
return array_values($this->data); |
186
|
|
|
} |
187
|
|
|
|
188
|
|
|
/** |
189
|
|
|
* {@inheritdoc} |
190
|
|
|
*/ |
191
|
1 |
|
public function getIterator(): \Traversable |
192
|
|
|
{ |
193
|
1 |
|
return $this->stream(); |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
/** |
197
|
|
|
* {@inheritdoc} |
198
|
|
|
*/ |
199
|
2 |
|
public function count(): int |
200
|
|
|
{ |
201
|
2 |
|
return count($this->data); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* {@inheritdoc} |
206
|
|
|
*/ |
207
|
2 |
|
public function stream(): StreamInterface |
208
|
|
|
{ |
209
|
2 |
|
return new ArrayStream(array_values($this->data)); |
210
|
|
|
} |
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* Create an HashSet with spl_object_hash as hash function |
214
|
|
|
* |
215
|
|
|
* @return HashSet |
216
|
|
|
* |
217
|
|
|
* @see spl_object_hash() |
218
|
|
|
*/ |
219
|
1 |
|
public static function spl(): HashSet |
220
|
|
|
{ |
221
|
1 |
|
return new static('spl_object_hash'); |
222
|
|
|
} |
223
|
|
|
} |
224
|
|
|
|