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 }