HashSet::clear()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 1
b 0
f 0
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