Passed
Push — master ( 833b6a...f70537 )
by Vincent
03:25
created

HashSet   A

Complexity

Total Complexity 22

Size/Duplication

Total Lines 155
Duplicated Lines 0 %

Test Coverage

Coverage 97.83%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 35
c 1
b 0
f 0
dl 0
loc 155
ccs 45
cts 46
cp 0.9783
rs 10
wmc 22

13 Methods

Rating   Name   Duplication   Size   Complexity  
A add() 0 10 2
A __construct() 0 3 2
A empty() 0 3 1
A lookup() 0 9 2
A clear() 0 3 1
A remove() 0 14 4
A forEach() 0 4 2
A stream() 0 3 1
A toArray() 0 3 1
A contains() 0 9 3
A getIterator() 0 3 1
A count() 0 3 1
A spl() 0 3 1
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
class HashSet implements SetInterface
63
{
64
    use CollectionTrait;
65
66
    /**
67
     * @var array
68
     */
69
    private $data = [];
70
71
    /**
72
     * @var callable
73
     */
74
    private $hashFunction;
75
76
77
    /**
78
     * HashSet constructor.
79
     *
80
     * @param callable $hashFunction The the hash function. Takes as parameter the element to hash, and should return a string
81
     */
82 49
    public function __construct(callable $hashFunction = null)
83
    {
84 49
        $this->hashFunction = $hashFunction ?: [Hash::class, 'compute'];
85 49
    }
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 49
    public function add($element): bool
91
    {
92 49
        $index = ($this->hashFunction)($element);
93
94 49
        if (isset($this->data[$index])) {
95 30
            return false;
96
        }
97
98 49
        $this->data[$index] = $element;
99 49
        return true;
100
    }
101
102
    /**
103
     * {@inheritdoc}
104
     */
105 7
    public function contains($element, bool $strict = false): bool
106
    {
107 7
        $key = ($this->hashFunction)($element);
108
109 7
        if (!isset($this->data[$key])) {
110 5
            return false;
111
        }
112
113 7
        return !$strict || $this->data[$key] === $element;
114
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 1
    public function lookup($element): OptionalInterface
120
    {
121 1
        $index = ($this->hashFunction)($element);
122
123 1
        if (!isset($this->data[$index])) {
124 1
            return Optional::empty();
125
        }
126
127 1
        return Optional::of($this->data[$index]);
128
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 3
    public function remove($element, bool $strict = false): bool
134
    {
135 3
        $index = ($this->hashFunction)($element);
136
137 3
        if (!isset($this->data[$index])) {
138 2
            return false;
139
        }
140
141 3
        if (!$strict || $this->data[$index] === $element) {
142 3
            unset($this->data[$index]);
143 3
            return true;
144
        }
145
146
        return false;
147
    }
148
149
    /**
150
     * {@inheritdoc}
151
     */
152 34
    public function clear(): void
153
    {
154 34
        $this->data = [];
155 34
    }
156
157
    /**
158
     * {@inheritdoc}
159
     */
160 1
    public function empty(): bool
161
    {
162 1
        return empty($this->data);
163
    }
164
165
    /**
166
     * {@inheritdoc}
167
     */
168 1
    public function forEach(callable $consumer): void
169
    {
170 1
        foreach ($this->data as $value) {
171 1
            $consumer($value);
172
        }
173 1
    }
174
175
    /**
176
     * {@inheritdoc}
177
     */
178 9
    public function toArray(): array
179
    {
180 9
        return array_values($this->data);
181
    }
182
183
    /**
184
     * {@inheritdoc}
185
     */
186 1
    public function getIterator()
187
    {
188 1
        return $this->stream();
189
    }
190
191
    /**
192
     * {@inheritdoc}
193
     */
194 2
    public function count(): int
195
    {
196 2
        return count($this->data);
197
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 2
    public function stream(): StreamInterface
203
    {
204 2
        return new ArrayStream(array_values($this->data));
205
    }
206
207
    /**
208
     * Create an HashSet with spl_object_hash as hash function
209
     *
210
     * @return HashSet
211
     *
212
     * @see spl_object_hash()
213
     */
214 1
    public static function spl(): HashSet
215
    {
216 1
        return new static('spl_object_hash');
217
    }
218
}
219