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 }