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 }