1 /**
2  * Hash Map
3  * Copyright: © 2014 Economic Modeling Specialists, Intl.
4  * Authors: Brian Schott
5  * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6  */
7 
8 module containers.hashmap;
9 
10 import containers.internal.hash : generateHash;
11 import containers.internal.node : shouldAddGCRange;
12 
13 /**
14  * Associative array / hash map.
15  * Params:
16  *     K = the key type
17  *     V = the value type
18  *     hashFunction = the hash function to use on the keys
19  */
20 struct HashMap(K, V, alias hashFunction = generateHash!K,
21 	bool supportGC = shouldAddGCRange!K || shouldAddGCRange!V)
22 {
23 	this(this) @disable;
24 
25 	/**
26 	 * Constructs an HashMap with an initial bucket count of bucketCount. bucketCount
27 	 * must be a power of two.
28 	 */
29 	this(size_t bucketCount)
30 	in
31 	{
32 		assert ((bucketCount & (bucketCount - 1)) == 0, "bucketCount must be a power of two");
33 	}
34 	body
35 	{
36 		initialize(bucketCount);
37 	}
38 
39 	~this()
40 	{
41 		import std.allocator : Mallocator, deallocate;
42 		foreach (ref bucket; buckets)
43 			typeid(typeof(bucket)).destroy(&bucket);
44 		static if (supportGC)
45 			GC.removeRange(buckets.ptr);
46 		Mallocator.it.deallocate(buckets);
47 	}
48 
49 	/**
50 	 * Supports $(B aa[key]) syntax.
51 	 */
52 	V opIndex(K key) const
53 	{
54 		import std.algorithm : find;
55 		import std.exception : enforce;
56 		import std.conv : text;
57 		if (buckets.length == 0)
58 			throw new Exception("'" ~ text(key) ~ "' not found in HashMap");
59 		size_t hash = generateHash(key);
60 		size_t index = hashToIndex(hash);
61 		foreach (r; buckets[index].range)
62 		{
63 			static if (storeHash)
64 			{
65 				if (r.hash == hash && r == key)
66 					return r.value;
67 			}
68 			else
69 			{
70 				if (r == key)
71 					return r.value;
72 			}
73 		}
74 		throw new Exception("'" ~ text(key) ~ "' not found in HashMap");
75 	}
76 
77 	/**
78 	 * Supports $(B aa[key] = value;) syntax.
79 	 */
80 	void opIndexAssign(V value, K key)
81 	{
82 		insert(key, value);
83 	}
84 
85 	/**
86 	 * Supports $(B key in aa) syntax.
87 	 */
88 	V* opBinaryRight(string op)(K key) const nothrow if (op == "in")
89 	{
90 		size_t hash = generateHash(key);
91 		size_t index = hashToIndex(hash);
92 		foreach (ref node; buckets[index].range)
93 		{
94 			static if (storeHash)
95 			{
96 				if (node.hash == hash && node == key)
97 					return &node.value;
98 			}
99 			else
100 			{
101 				if (node == key)
102 					return &node.value;
103 			}
104 		}
105 		return null;
106 	}
107 
108 	/**
109 	 * Removes the value associated with the given key
110 	 * Returns: true if a value was actually removed.
111 	 */
112 	bool remove(K key)
113 	{
114 		if (buckets.length == 0)
115 			return false;
116 		size_t hash = generateHash(key);
117 		size_t index = hashToIndex(hash);
118 		static if (storeHash)
119 			bool removed = buckets[index].remove(Node(hash, key));
120 		else
121 			bool removed = buckets[index].remove(Node(key));
122 		if (removed)
123 			_length--;
124 		return removed;
125 	}
126 
127 	/**
128 	 * Returns: the number of key/value pairs in this aa
129 	 */
130 	size_t length() const nothrow pure @property @safe @nogc
131 	{
132 		return _length;
133 	}
134 
135 	/**
136 	 * Returns: a GC-allocated array filled with the keys contained in this map.
137 	 */
138 	K[] keys() const @property
139 	out(result)
140 	{
141 		assert (result.length == _length);
142 	}
143 	body
144 	{
145 		import std.array : appender;
146 		auto app = appender!(K[])();
147 		foreach (ref const bucket; buckets)
148 		{
149 			foreach (item; bucket.range)
150 				app.put(item.key);
151 		}
152 		return app.data;
153 	}
154 
155 	/**
156 	 * Returns: a GC-allocated array containing the values contained in this map.
157 	 */
158 	V[] values() const @property
159 	out(result)
160 	{
161 		assert (result.length == _length);
162 	}
163 	body
164 	{
165 		import std.array : appender;
166 		auto app = appender!(V[])();
167 		foreach (ref const bucket; buckets)
168 		{
169 			foreach (item; bucket.range)
170 				app.put(item.value);
171 		}
172 		return app.data;
173 	}
174 
175 	/**
176 	 * Support for $(D foreach(key, value; aa) { ... }) syntax;
177 	 */
178 	int opApply(int delegate(ref K, ref V) del)
179 	{
180 		int result = 0;
181 		outer: foreach (ref bucket; buckets)
182 		{
183 			foreach (ref node; bucket.range)
184 			{
185 				result = del(node.key, node.value);
186 				if (result != 0)
187 					return result;
188 			}
189 		}
190 		return result;
191 	}
192 
193 private:
194 
195 	import std.allocator : Mallocator, allocate;
196 	import std.traits : isBasicType;
197 	import containers.unrolledlist : UnrolledList;
198 	import core.memory : GC;
199 
200 	enum bool storeHash = !isBasicType!K;
201 
202 	void initialize(size_t bucketCount = 4)
203 	{
204 		import std.conv : emplace;
205 		import std.allocator : allocate;
206 		buckets = (cast(Bucket*) Mallocator.it.allocate( // Valgrind
207 			bucketCount * Bucket.sizeof))[0 .. bucketCount];
208 		assert (buckets.length == bucketCount);
209 		static if (supportGC)
210 			GC.addRange(buckets.ptr, buckets.length * Bucket.sizeof);
211 		foreach (ref bucket; buckets)
212 			emplace(&bucket);
213 	}
214 
215 	void insert(K key, V value)
216 	{
217 		if (buckets.length == 0)
218 			initialize();
219 		size_t hash = generateHash(key);
220 		size_t index = hashToIndex(hash);
221 		foreach (ref item; buckets[index].range)
222 		{
223 			static if (storeHash)
224 			{
225 				if (item.hash == hash && item.key == key)
226 				{
227 					item.value = value;
228 					return;
229 				}
230 			}
231 			else
232 			{
233 				if (item.key == key)
234 				{
235 					item.value = value;
236 					return;
237 				}
238 			}
239 		}
240 		static if (storeHash)
241 			buckets[index].put(Node(hash, key, value));
242 		else
243 			buckets[index].put(Node(key, value));
244 		_length++;
245 		if (shouldRehash)
246 			rehash();
247 	}
248 
249 	/**
250 	 * Returns: true if the load factor has been exceeded
251 	 */
252 	bool shouldRehash() const pure nothrow @safe @nogc
253 	{
254 		return cast(float) _length / cast(float) buckets.length > 0.75;
255 	}
256 
257 	/**
258 	 * Rehash the map.
259 	 */
260 	void rehash() @trusted
261 	{
262 		import std.allocator : allocate, deallocate;
263 		import std.conv : emplace;
264 		immutable size_t newLength = buckets.length << 1;
265 		immutable size_t newSize = newLength * Bucket.sizeof;
266 		Bucket[] oldBuckets = buckets;
267 		assert (oldBuckets.ptr == buckets.ptr);
268 		buckets = cast(Bucket[]) Mallocator.it.allocate(newSize);
269 		static if (supportGC)
270 			GC.addRange(buckets.ptr, buckets.length * Bucket.sizeof);
271 		assert (buckets);
272 		assert (buckets.length == newLength);
273 		foreach (ref bucket; buckets)
274 			emplace(&bucket);
275 		foreach (ref bucket; oldBuckets)
276 		{
277 			foreach (node; bucket.range)
278 			{
279 				static if (storeHash)
280 				{
281 					size_t index = hashToIndex(node.hash);
282 					buckets[index].put(Node(node.hash, node.key, node.value));
283 				}
284 				else
285 				{
286 					size_t hash = generateHash(node.key);
287 					size_t index = hashToIndex(hash);
288 					buckets[index].put(Node(node.key, node.value));
289 				}
290 			}
291 			typeid(typeof(bucket)).destroy(&bucket);
292 		}
293 		static if (supportGC)
294 			GC.removeRange(oldBuckets.ptr);
295 		Mallocator.it.deallocate(cast(void[]) oldBuckets);
296 	}
297 
298 	size_t hashToIndex(size_t hash) const pure nothrow @safe @nogc
299 	in
300 	{
301 		assert (buckets.length > 0);
302 	}
303 	out (result)
304 	{
305 		assert (result < buckets.length);
306 	}
307 	body
308 	{
309 		return hash & (buckets.length - 1);
310 	}
311 
312 	size_t hashIndex(K key) const
313 	out (result)
314 	{
315 		assert (result < buckets.length);
316 	}
317 	body
318 	{
319 		return hashToIndex(generateHash(key));
320 	}
321 
322 	struct Node
323 	{
324 		bool opEquals(ref const K key) const
325 		{
326 			return key == this.key;
327 		}
328 
329 		bool opEquals(ref const Node n) const
330 		{
331 			static if (storeHash)
332 				return this.hash == n.hash && this.key == n.key;
333 			else
334 				return this.key == n.key;
335 		}
336 
337 		static if (storeHash)
338 			size_t hash;
339 		K key;
340 		V value;
341 	}
342 
343 	alias Bucket = UnrolledList!(Node, supportGC);
344 	Bucket[] buckets;
345 	size_t _length;
346 }
347 
348 ///
349 unittest
350 {
351 	import std.uuid : randomUUID;
352 	auto hm = HashMap!(string, int)(16);
353 	assert (hm.length == 0);
354 	assert (!hm.remove("abc"));
355 	hm["answer"] = 42;
356 	assert (hm.length == 1);
357 	assert ("answer" in hm);
358 	hm.remove("answer");
359 	assert (hm.length == 0);
360 	hm["one"] = 1;
361 	hm["one"] = 1;
362 	assert (hm.length == 1);
363 	assert (hm["one"] == 1);
364 	foreach (i; 0 .. 1000)
365 	{
366 		hm[randomUUID().toString] = i;
367 	}
368 	assert (hm.length == 1001);
369 	assert (hm.keys().length == hm.length);
370 	assert (hm.values().length == hm.length);
371 	foreach (ref k, ref v; hm) {}
372 
373 	auto hm2 = HashMap!(char, char)(4);
374 	hm2['a'] = 'a';
375 }