module ecs.hash_map; import std.traits; import ecs.vector; import ecs.traits; enum doNotInline = "version(DigitalMars)pragma(inline,false);version(LDC)pragma(LDC_never_inline);"; private enum HASH_EMPTY = 0; private enum HASH_DELETED = 0x1; private enum HASH_FILLED_MARK = ulong(1) << 8 * ulong.sizeof - 1; export ulong defaultHashFunc(T)(auto ref T t) nothrow @nogc { static if (isIntegral!(T)) { return hashInt(t); } else { return hashInt(t.hashOf); // hashOf is not giving proper distribution between H1 and H2 hash parts } } // Can turn bad hash function to good one export ulong hashInt(ulong x) nothrow @nogc @safe { x = (x ^ (x >> 30)) * 0xbf58476d1ce4e5b9; x = (x ^ (x >> 27)) * 0x94d049bb133111eb; x = x ^ (x >> 31); return x; } struct HashMap(KeyPar, ValuePar, alias hashFunc = defaultHashFunc) { alias Key = KeyPar; alias Value = ValuePar; enum rehashFactor = 0.75; enum size_t getIndexEmptyValue = size_t.max; static struct KeyVal { Key key; Value value; } static struct Bucket { ulong hash; KeyVal keyValue; } Vector!Bucket elements; // Length should be always power of 2 size_t length; // Used to compute loadFactor size_t markerdDeleted; export void clear() { elements.clear(); length = 0; markerdDeleted = 0; } export void reset() { elements.reset(); length = 0; markerdDeleted = 0; } export bool isIn(ref Key el) { return getIndex(el) != getIndexEmptyValue; } export bool isIn(Key el) { return getIndex(el) != getIndexEmptyValue; } export Value* getPtr()(auto ref Key k) { size_t index = getIndex(k); if (index == getIndexEmptyValue) { return null; } else { return &elements[index].keyValue.value; } } export ref Value get()(auto ref Key k) { size_t index = getIndex(k); assert(index != getIndexEmptyValue); return elements[index].keyValue.value; } deprecated("Use get with second parameter.") export auto ref Value getDefault()( auto ref Key k, auto ref Value defaultValue) { return get(k, defaultValue); } export auto ref Value get()(auto ref Key k, auto ref Value defaultValue) { size_t index = getIndex(k); if (index == getIndexEmptyValue) { return defaultValue; } else { return elements[index].keyValue.value; } } export ref Value getInsertDefault()(auto ref Key k, auto ref Value defaultValue) { size_t index = getIndex(k); if (index == getIndexEmptyValue) { add(k, defaultValue); } index = getIndex(k); assert(index != getIndexEmptyValue); return elements[index].keyValue.value; } export bool tryRemove(Key el) { size_t index = getIndex(el); if (index == getIndexEmptyValue) { return false; } length--; elements[index].hash = HASH_DELETED; markerdDeleted++; return true; } export void remove(Key el) { bool ok = tryRemove(el); assert(ok); } export ref Value opIndex()(auto ref Key key) { return get(key); } export void opIndexAssign()(auto ref Value value, auto ref Key key) { add(key, value); } export void add()(auto ref Key key, auto ref Value value) { size_t index = getIndex(key); if (index != getIndexEmptyValue) { elements[index].keyValue.value = value; return; } if (getLoadFactor(length + 1) > rehashFactor || getLoadFactor(length + markerdDeleted) > rehashFactor) { rehash(); } length++; immutable ulong hash = hashFunc(key) | HASH_FILLED_MARK; immutable size_t rotateMask = elements.length - 1; index = hash & rotateMask; // Starting point while (true) { Bucket* gr = &elements[index]; if ((gr.hash & HASH_FILLED_MARK) == 0) { if (gr.hash == HASH_DELETED) { markerdDeleted--; } gr.hash = hash; gr.keyValue.key = key; gr.keyValue.value = value; return; } index++; index = index & rotateMask; } } // For debug //int numA; //int numB; export size_t getIndex(Key el) { return getIndex(el); } export size_t getIndex(ref Key el) { mixin(doNotInline); immutable size_t groupsLength = elements.length; if (groupsLength == 0) { return getIndexEmptyValue; } immutable ulong hash = hashFunc(el) | HASH_FILLED_MARK; immutable size_t rotateMask = groupsLength - 1; size_t index = hash & rotateMask; // Starting point //numA++; while (true) { //numB++; Bucket* gr = &elements[index]; if (gr.hash == hash && gr.keyValue.key == el) { return index; } if (gr.hash == HASH_EMPTY) { return getIndexEmptyValue; } index++; index = index & rotateMask; } } export float getLoadFactor(size_t forElementsNum) { if (elements.length == 0) { return 1; } return cast(float) forElementsNum / (elements.length); } export void rehash() { mixin(doNotInline); // Get all elements Vector!KeyVal allElements; allElements.reserve(elements.length); foreach (ref Bucket el; elements) { if ((el.hash & HASH_FILLED_MARK) == 0) { el.hash = HASH_EMPTY; continue; } el.hash = HASH_EMPTY; allElements ~= el.keyValue; } if (getLoadFactor(length + 1) > rehashFactor) { // Reallocate elements.length = (elements.length ? elements.length : 4) << 1; // Power of two, initially 8 elements } // Insert elements foreach (i, ref el; allElements) { add(el.key, el.value); } length = allElements.length; markerdDeleted = 0; } // foreach support export int opApply(DG)(scope DG dg) { int result; foreach (ref Bucket gr; elements) { if ((gr.hash & HASH_FILLED_MARK) == 0) { continue; } static if (isForeachDelegateWithTypes!(DG, Key)) { result = dg(gr.keyValue.key); } else static if (isForeachDelegateWithTypes!(DG, Value)) { result = dg(gr.keyValue.value); } else static if (isForeachDelegateWithTypes!(DG, Key, Value)) { result = dg(gr.keyValue.key, gr.keyValue.value); } else { static assert(0); } if (result) break; } return result; } export int byKey(scope int delegate(Key k) dg) { int result; foreach (ref Key k; this) { result = dg(k); if (result) break; } return result; } export int byValue(scope int delegate(ref Value k) dg) { int result; foreach (ref Value v; this) { result = dg(v); if (result) break; } return result; } export int byKeyValue(scope int delegate(ref Key k, ref Value v) dg) { int result; foreach (ref Key k, ref Value v; this) { result = dg(k, v); if (result) break; } return result; } import std.format : FormatSpec, formatValue; /** * Preety print */ export void toString(scope void delegate(const(char)[]) sink, FormatSpec!char fmt) { formatValue(sink, '[', fmt); foreach (ref k, ref v; &byKeyValue) { formatValue(sink, k, fmt); formatValue(sink, ':', fmt); formatValue(sink, v, fmt); formatValue(sink, ", ", fmt); } formatValue(sink, ']', fmt); } } static void dumpHashMapToJson(T)(ref T map, string path = "HashMapDump.json") { Vector!char data; import std.file; import mutils.serializer.json; JSONSerializer.instance.serialize!(Load.no)(map, data); std.file.write(path, data[]); } static void printHashMap(T)(ref T map) { import std.stdio; writeln(T.stringof, " dump:\n"); foreach (k, v; &map.byKeyValue) { writefln("%20s : %20s", k, v); } } unittest { HashMap!(int, int) map; assert(map.isIn(123) == false); assert(map.markerdDeleted == 0); map.add(123, 1); map.add(123, 1); assert(map.isIn(123) == true); assert(map.isIn(122) == false); assert(map.length == 1); map.remove(123); assert(map.markerdDeleted == 1); assert(map.isIn(123) == false); assert(map.length == 0); assert(map.tryRemove(500) == false); map.add(123, 1); assert(map.markerdDeleted == 0); assert(map.tryRemove(123) == true); foreach (i; 1 .. 130) { map.add(i, 1); } foreach (i; 1 .. 130) { assert(map.isIn(i)); } foreach (i; 130 .. 500) { assert(!map.isIn(i)); } foreach (int el; map) { assert(map.isIn(el)); } } unittest { HashMap!(int, int) map; map.add(1, 10); assert(map.get(1) == 10); assert(map.get(2, 20) == 20); assert(!map.isIn(2)); assert(map.getInsertDefault(2, 20) == 20); assert(map.get(2) == 20); map[5] = 50; assert(map[5] == 50); foreach (k; &map.byKey) { } foreach (k, v; &map.byKeyValue) { } foreach (v; &map.byValue) { } } unittest { HashMap!(Vector!char, int) map; Vector!char vecA; vecA ~= "AAA"; map.add(vecA, 10); assert(map[vecA] == 10); map.add(vecA, 20); assert(map[vecA] == 20); //assert(vecA=="AAA"); //assert(map["AAA"]==10);// TODO hashMap Vector!char and string }