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