1 module viva.collections.table;
2 
3 import viva.exceptions.check : checkEquals;
4 
5 import std.typecons;
6 import std.traits;
7 
8 /++
9  + Enum of all entry value types
10  +/
11 enum EntryValueType : byte
12 {
13     null_,
14     string_,
15     integer_,
16     uinteger_,
17     float_,
18     true_,
19     false_
20 }
21 
22 /++
23  + Object for holding the entry value
24  +/
25 struct EntryValue
26 {
27     /++
28      + Takes in the value
29      + Params:
30      +      value = The value of the object
31      +/
32     this(T)(T value)
33     {
34         assign(value);
35     }
36 
37     /++
38      + Union for storing each type of value
39      +/
40     union Store
41     {
42         /++
43          + String value
44          +/
45         string str;
46 
47         /++
48          + Integer/long value
49          +/
50         long integer;
51         
52         /++
53          + Uinteger/ulong value
54          +/
55         ulong uinteger;
56         
57         /++
58          + Floating/double value
59          +/
60         double floating;
61     }
62 
63     private {
64         Store store;
65         EntryValueType valueType;
66     }
67 
68     /++
69      + Get the values `EntryValueType` type
70      + Returns: The type of value from the `EntryValueType` enum
71      +/
72     @property EntryValueType type() const pure nothrow @safe @nogc
73     {
74         return valueType;
75     }
76 
77     /++
78      + Get the string value, if the value is a string
79      + Returns: The value as a string
80      +/
81     @property string str() const pure @trusted
82     {
83         checkEquals(valueType, EntryValueType.string_, "EntryValue is not a string!");
84         return store.str;
85     }
86 
87     /++
88      + Get the int/long value, if the value is a int/long
89      + Returns: The value as a long
90      +/
91     @property long integer() const pure @safe
92     {
93         checkEquals(valueType, EntryValueType.integer_, "EntryValue is not an integer!");
94         return store.integer;
95     }
96 
97     /++
98      + Get the uint/ulong value, if the value is a uing/ulong
99      + Returns: The value as a ulong
100      +/
101     @property ulong uinteger() const pure @safe
102     {
103         checkEquals(valueType, EntryValueType.uinteger_, "EntryValue is not an unsigned integer!");
104         return store.uinteger;
105     }
106 
107     /++
108      + Get the float/double value, if the value is a float/double
109      + Returns: The value as a double
110      +/
111     @property double floating() @safe const pure
112     {
113         checkEquals(valueType, EntryValueType.float_, "EntryValue is not a float!");
114         return store.floating;
115     }
116 
117     /++
118      + Get the bool value, if the value is a bool
119      + Returns: The value as a boolean
120      +/
121     @property bool boolean() @safe const pure
122     {
123         if (valueType == EntryValueType.true_) return true;
124         if (valueType == EntryValueType.false_) return false;
125 
126         // TODO: Throw exception
127         return false;
128     }
129 
130     /++
131      + Returns: Whether or not the entries value is null
132      +/
133     @property bool isNull() @safe @nogc const pure nothrow
134     {
135         return valueType == EntryValueType.null_;
136     }
137 
138     /++
139      + Gets the value of the object, depending on the type given (`T`)
140      +/
141     @property inout(T) get(T)() @safe inout const pure
142     {
143         static if (is(Unqual!T == string))
144         {
145             return str;
146         }
147         else static if (is(Unqual!T == bool))
148         {
149             return boolean;
150         }
151         else static if (isFloatingPoint!T)
152         {
153             switch(valueType)
154             {
155                 case EntryValueType.float_: return cast(T) floating;
156                 case EntryValueType.integer_: return cast(T) integer;
157                 case EntryValueType.uinteger_: return cast(T) uinteger;
158                 default:
159                 {
160                     return null;
161                     // TODO: Throw exception
162                 }
163             }
164         }
165         else static if (__traits(isUnsigned, T))
166         {
167             return cast(T) uinteger;
168         }
169         else static if (isSigned!T)
170         {
171             return cast(T) integer;
172         }
173         else
174         {
175             static assert(false, "Unsupported type!");
176         }
177     }
178 
179     private void assign(T)(T value)
180     {
181         static if (is(T : typeof(null)))
182         {
183             valueType = EntryValueType.null_;
184         }
185         else static if (is(T : string))
186         {
187             valueType = EntryValueType.string_;
188             string t = value;
189             () @trusted { store.str = t; }();
190         }
191         else static if (is(T : bool))
192         {
193             valueType = value ? EntryValueType.true_ : EntryValueType.false_;
194         }
195         else static if (is(T : ulong) && isUnsigned!T)
196         {
197             valueType = EntryValueType.uinteger_;
198             store.uinteger = value;
199         }
200         else static if (is(T : long))
201         {
202             valueType = EntryValueType.integer_;
203             store.integer = value;
204         }
205         else static if (isFloatingPoint!T)
206         {
207             valueType = EntryValueType.float_;
208             store.floating = value;
209         }
210         else static if (is(T : EntryValue))
211         {
212             valueType = value.type;
213             store = value.store;
214         }
215         else
216         {
217             static assert(false, "Error message here...");
218         }
219     }
220 
221     string toString() const @safe
222     {
223         import std.conv : to;
224 
225         final switch (valueType)
226         {
227             case EntryValueType.null_: return "null";
228             case EntryValueType.string_: return str;
229             case EntryValueType.integer_: return integer.to!string;
230             case EntryValueType.uinteger_: return uinteger.to!string;
231             case EntryValueType.float_: return floating.to!string;
232             case EntryValueType.true_: return "true";
233             case EntryValueType.false_: return "false";
234         }
235     }
236 }
237 
238 /++
239  + The Entry object holds the needed information about all table entries
240  +/
241 struct Entry
242 {
243     /++
244      + The key/name of the entry
245      +/
246     string key;
247     
248     /++
249      + The value of the entry, which may be null
250      +/
251     Nullable!EntryValue value;
252 }
253 
254 /++
255  + A hash table associates a set of keys with a set of values.
256  + Each key/value pair is an entry in the table. Given a key, you can look up its corresponding value.
257  + You can add new key/value pairs and remove entries by key.
258  + If you add a new value for an existing key, it replaces the previous entry.
259  +
260  + The table will automatically resize when it's close to being full, which means
261  + you can never fill up the table.
262  +/
263 struct HashTable
264 {
265     /++
266      + The size of the table. This keeps tracks of how many non-empty entries the table has
267      +/
268     int count;
269     
270     /++
271      + The max amount of entries in the table
272      +/
273     int capacity;
274     
275     /++
276      + An array of all the entries
277      +/
278     Entry[] entries;
279 
280     /++
281      + Takes in the default capacity
282      + Params:
283      +      capacity = The default capacity for the table
284      +/
285     this(int capacity)
286     {
287         this.count = 0;
288         this.capacity = capacity;
289         this.entries = new Entry[capacity];
290     }
291 
292     /++
293      + Looks up an entry in the array, and returns it if it's key is empty or matching
294      + Params:
295      +      key = The key to look for in the entries
296      + Returns: A pointer to the found entry
297      +/
298     Entry* findEntry(string key)
299     {
300         uint index = hashFNV1A(key) % capacity;
301 
302         for (;;)
303         {
304             Entry* entry = &entries[index];
305             
306             if (entry.key == key || entry.key == "")
307             {
308                 return entry;
309             }
310 
311             index = (index + 1) % capacity;
312         }
313     }
314 
315     /++
316      + Finds the index of the entry
317      + Params:
318      +      key = The key of the entry to look for
319      + Returns: The index of the entry in the `entries` array
320      +/
321     uint findEntryIndex(string key)
322     {
323         uint index = hashFNV1A(key) % capacity;
324 
325         for (;;)
326         {
327             Entry* entry = &entries[index];
328             
329             if (entry.key == key || entry.key == "")
330             {
331                 return index;
332             }
333 
334             index = (index + 1) % capacity;
335         }
336     }
337 
338     EntryValue[] getAll()
339     {
340         EntryValue[] values = new EntryValue[count];
341 
342         int i;
343         foreach (entry; entries)
344         {
345             if (!entry.value.isNull)
346             {
347                 values[i] = entry.value.get;
348                 i++;
349             }
350         }
351 
352         return values;
353     }
354 
355     /++
356      + Checks if the table contains a certain element
357      + Params:
358      +      key = The key to find
359      + Returns: `false` if the key doesn't exist and `true` if it does exist
360      +/
361     bool hasEntry(string key)
362     {
363         return !findEntry(key).value.isNull;
364     }
365 
366     private void adjustCapacity(int newCapacity)
367     {
368         Entry[] entriesHolder = entries;
369         entries = new Entry[newCapacity];
370         for (int i = 0; i < entriesHolder.length; i++)
371         {
372             const(Entry)* entry = &entriesHolder[i];
373             if (entry.key == "") continue;
374             
375             entries[i].key = entriesHolder[i].key;
376             entries[i].value = entriesHolder[i].value;
377         }
378 
379         capacity = newCapacity;
380     }
381 
382     /++
383      + Adds a new entry, with the given name and value, to the table
384      + Params:
385      +      key = The name of the entry
386      +      value = The raw value of the entry
387      + Returns: A bool indicating whether or not the entry is new or already exists
388      +/
389     bool set(T)(string key, T value)
390     {
391         return set(key, EntryValue(value));
392     }
393 
394     /++
395      + Adds a new entry, with the given name and value, to the table
396      + Params:
397      +      key = The name of the entry
398      +      value = The entry value of the entry
399      + Returns: A bool indicating whether or not the entry is new or already exists
400      +/
401     bool set(string key, EntryValue value)
402     {
403         if (count > capacity * 0.75)
404         {
405             adjustCapacity(growCapacity(capacity));
406         }
407 
408         Entry* entry = findEntry(key);
409 
410         const(bool) isNewKey = entry.key == "";
411         if (isNewKey) count++;
412 
413         entry.key = key;
414         entry.value = value.nullable;
415         return isNewKey;
416     }
417 
418     /++
419      + Merges the entries of another table into the current tables entries
420      + Params:
421      +      table = The other table
422      +/
423     void merge(HashTable table)
424     {
425         for (int i = 0; i < table.capacity; i++)
426         {
427             Entry entry = table.entries[i];
428             if (entry.key != "")
429             {
430                 set(entry.key, entry.value.get);
431             }
432         }
433     }
434 
435     /++
436      + Gets an entry from the table
437      + Params:
438      +      key = The name of the entry
439      + Returns: A nullable `EntryValue` object
440      +/
441     Nullable!EntryValue get(string key)
442     {
443         if (count == 0) return Nullable!EntryValue.init;
444         return findEntry(key).value.nullable.get;
445     }
446 
447     /++
448      + Removes an item from the table
449      + Params:
450      +      key = The name of the entry to be removed
451      + Returns: `false` if the key doesn't exist and `true` if it does exist
452      +/
453     bool remove(string key)
454     {
455         if (hasEntry(key))
456         {
457             count--;
458             // TODO: Adjust capacity
459 
460             uint index = findEntryIndex(key);
461             entries[index] = Entry("", Nullable!EntryValue.init);
462 
463             return true;
464         }
465 
466         return false;
467     }
468 }
469 
470 private uint growCapacity(uint capacity)
471 {
472     return capacity < 8 ? 8 : capacity * 2;
473 }
474 
475 private uint hashFNV1A(string s) pure nothrow @safe
476 {
477     uint hash = 2_166_136_261;
478 
479     foreach (c; s)
480     {
481         hash ^= c;
482         hash *= 16_777_619;
483     }
484 
485     return hash;
486 }