From e6f2254c35e23ead8d2d3badd512fc5951c52a9c Mon Sep 17 00:00:00 2001 From: Anton Tarasenko Date: Sun, 3 Jul 2022 00:07:24 +0700 Subject: [PATCH] Add `HashTable` colelction --- sources/Data/Collections/HashTable.uc | 1075 +++++++++++++++++ sources/Data/Collections/HashTableIterator.uc | 93 ++ 2 files changed, 1168 insertions(+) create mode 100644 sources/Data/Collections/HashTable.uc create mode 100644 sources/Data/Collections/HashTableIterator.uc diff --git a/sources/Data/Collections/HashTable.uc b/sources/Data/Collections/HashTable.uc new file mode 100644 index 0000000..aee142d --- /dev/null +++ b/sources/Data/Collections/HashTable.uc @@ -0,0 +1,1075 @@ +/** + * This class implements an associative array for storing arbitrary types + * of data that provides a quick (near constant) access to *values* by + * associated *keys*. + * Since UnrealScript lacks any sort of templating, `HashTable` + * stores generic `AcediaObject` keys and values. `Text` can be used instead of + * typical `string` keys and primitive values can be added in their boxed form + * (either as actual `Box` or as it's reference counterpart). + * Copyright 2022 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class HashTable extends Collection; + +// Defines key <-> value mapping +struct Entry +{ + var AcediaObject key; + var AcediaObject value; +}; + +// Bucket of entries. +// Used to store entries with the same index in hash table. +struct Bucket +{ + var array entries; +}; +var private array hashTable; + +// Amount of elements currently stored in this `HashTable`. +// If one of the keys was deallocated outside of `HashTable`, +// this value may overestimate actual amount of elements. +var private int storedElementCount; +// Lower limit on hash table capacity, can be changed by the user. +var private int minimalCapacity; + +// hard lower and upper limits on hash table size, constant. +var private const int MINIMUM_SIZE; +var private const int MAXIMUM_SIZE; +// Minimum and maximum allowed density of elements +// (`storedElementCount / hashTable.length`). +// If density falls outside this range, - we have to resize hash table to +// get into (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds, as long as it does not +// violate hard size restrictions. +// Actual size changes in multipliers of 2, so +// `MINIMUM_DENSITY * 2 < MAXIMUM_DENSITY` must hold or we will constantly +// oscillate outside of (MINIMUM_DENSITY; MAXIMUM_DENSITY) bounds. +var private const float MINIMUM_DENSITY; +var private const float MAXIMUM_DENSITY; + +/** + * Auxiliary struct, necessary to implement iterator for `HashTable`. + * Can be used for manual iteration, but should be avoided in favor of + * `Iterator`. + */ +struct Index +{ + var protected int bucketIndex; + var protected int entryIndex; +}; + +protected function Constructor() +{ + UpdateHashTableSize(); +} + +protected function Finalizer() +{ + Empty(); +} + +// Auxiliary method that is needed as a replacement for `%` module +// operator, since it is an operation on `float`s in UnrealScript and does not +// have appropriate value range to work with hashes. +// Assumes non-negative input. +private function int Remainder(int number, int divisor) +{ + local int quotient; + + quotient = number / divisor; + return (number - quotient * divisor); +} + +// Calculates appropriate bucket index for the given key. +// Assumes that given key is not `none` and is allocated. +private final function int GetBucketIndex(AcediaObject key) +{ + local int bucketIndex; + + bucketIndex = key.GetHashCode(); + if (bucketIndex < 0) { + // Minimum `int` value is greater than maximum one in absolute value, + // so shift it up to avoid overflow. + bucketIndex = -1 * (bucketIndex + 1); + } + bucketIndex = Remainder(bucketIndex, hashTable.length); + return bucketIndex; +} + +// Accessing value in `HashTable` requires two level lookup of both bucket and +// entry (inside that bucket) indices. +// +// As a result returns bucket's and entry's indices in `bucketIndex` and +// `entryIndex` inside `out` variables. +// `bucketIndex` is guaranteed to be found for non-`none` keys (in case you +// want to add a new entry), `entryIndex` is valid iff method returns `true`, +// otherwise it's equal to the index at which new property can get inserted. +private final function bool FindEntryIndices( + AcediaObject key, + out int bucketIndex, + out int entryIndex) +{ + local int i; + local array bucketEntries; + + if (key == none){ + return false; + } + bucketIndex = GetBucketIndex(key); + // Check if bucket actually has given key. + bucketEntries = hashTable[bucketIndex].entries; + for (i = 0; i < bucketEntries.length; i += 1) + { + if (key.IsEqual(bucketEntries[i].key)) + { + entryIndex = i; + return true; + } + } + entryIndex = bucketEntries.length; + return false; +} + +// Checks if we need to change our current hash table size +// and does so if needed +private final function UpdateHashTableSize() +{ + local int oldSize, newSize; + + oldSize = hashTable.length; + // Calculate new size (and whether it is needed) based on amount of + // stored properties and current size + newSize = oldSize; + if (storedElementCount < newSize * MINIMUM_DENSITY) { + newSize /= 2; + } + else if (storedElementCount > newSize * MAXIMUM_DENSITY) { + newSize *= 2; + } + // `table_density = items_amount / table_size`, so to store at least + // `items_amount = minimalCapacity` without making table too dense we need + // `table_size = minimalCapacity / MAXIMUM_DENSITY`. + newSize = Max(newSize, Ceil(minimalCapacity / MAXIMUM_DENSITY)); + // But everything must fall into the set hard limits + newSize = Clamp(newSize, MINIMUM_SIZE, MAXIMUM_SIZE); + // Only resize if difference is huge enough or table does not exists yet + if (newSize != oldSize) { + ResizeHashTable(newSize); + } +} + +// Changes size of the hash table, does not check any limits, +// does not check if `newSize` is a valid size (`newSize > 0`). +private final function ResizeHashTable(int newSize) +{ + local int i, j; + local int newBucketIndex, newEntryIndex; + local array bucketEntries; + local array oldHashTable; + + oldHashTable = hashTable; + // Clean current hash table + hashTable.length = 0; + hashTable.length = newSize; + for (i = 0; i < oldHashTable.length; i += 1) + { + bucketEntries = oldHashTable[i].entries; + for (j = 0; j < bucketEntries.length; j += 1) + { + newBucketIndex = GetBucketIndex(bucketEntries[j].key); + newEntryIndex = hashTable[newBucketIndex].entries.length; + hashTable[newBucketIndex].entries[newEntryIndex] = bucketEntries[j]; + } + } +} + +/** + * Returns minimal capacity of the caller associative array. + * + * See `SetMinimalCapacity()` for details. + * + * @return Minimal capacity of the caller associative array. Default is zero. + */ +public final function int GetMinimalCapacity() +{ + return minimalCapacity; +} + +/** + * Returns minimal capacity of the caller associative array. + * + * This associative array works like a hash table and needs to allocate + * sufficiently large dynamic array as a storage for its items. + * If you keep adding new items that storage will eventually become too small + * for hash table to work efficiently and we will have to reallocate and + * re-fill it. If you want to add a huge enough amount of items, this process + * can be repeated several times. + * This is not ideal, since it means doing a lot of iteration, each + * increasing infinite loop counter (game will crash if it gets high enough). + * Setting minimal capacity to the (higher) amount of items you expect to + * store in the caller array can remove the need for reallocating the storage. + * + * @param newMinimalCapacity New minimal capacity of this associative array. + * It's recommended to set it to the max amount of items you expect to + * store in this associative array + * (you will be still allowed to store more). + */ +public final function SetMinimalCapacity(int newMinimalCapacity) +{ + minimalCapacity = newMinimalCapacity; + UpdateHashTableSize(); +} + +/** + * Checks if caller `HashTable` has value recorded with a given `key`. + * + * @return `true` if caller `HashTable` has value recorded with + * a given `key` and `false` otherwise. + */ +public final function bool HasKey(AcediaObject key) +{ + local int bucketIndex, entryIndex; + + return FindEntryIndices(key, bucketIndex, entryIndex); +} + +/** + * Returns borrowed value recorded by a given key `key` in the caller + * `HashTable`. + * + * Can return `none` if either stored values is `none` or there's no value + * recorded with a `key`. To check whether there is a record, corresponding to + * the `key` use `HasKey()` method. + * + * @param key Key for which to return value. + * @return Value, stored with given key `key`. If there is no value with + * such a key method will return `none`. + */ +private final function AcediaObject BorrowItem(AcediaObject key) +{ + local int bucketIndex, entryIndex; + + if (FindEntryIndices(key, bucketIndex, entryIndex)) { + return hashTable[bucketIndex].entries[entryIndex].value; + } + return none; +} + +/** + * Returns value recorded by a given key `key` in the caller + * `HashTable`. + * + * Can return `none` if either stored values is `none` or there's no value + * recorded with a `key`. To check whether there is a record, corresponding to + * the `key` use `HasKey()` method. + * + * @param key Key for which to return value. + * @return Value, stored with given key `key`. If there is no value with + * such a key method will return `none`. + */ +public final function AcediaObject GetItem(AcediaObject key) +{ + local int bucketIndex, entryIndex; + local AcediaObject result; + + if (FindEntryIndices(key, bucketIndex, entryIndex)) { + result = hashTable[bucketIndex].entries[entryIndex].value; + } + if (result != none) { + return result.NewRef(); + } + return none; +} + +/** + * Returns entry corresponding to a given key `key` in the caller + * `HashTable`. + * + * @param key Key for which to return entry. + * @return Entry (key/value pair) with the given key `key`. + */ +public final function Entry GetEntry(AcediaObject key) +{ + local Entry result; + local int bucketIndex, entryIndex; + + if (!FindEntryIndices(key, bucketIndex, entryIndex)) { + return result; + } + result = hashTable[bucketIndex].entries[entryIndex]; + if (result.key != none) { + result.key.NewRef(); + } + if (result.value != none) { + result.value.NewRef(); + } + return result; +} + +/** + * Returns entry corresponding to a given key `key` in the caller + * `HashTable`, removing it from the caller `HashTable`. + * + * @param key Key for which to return entry. + * @return Entry (key/value pair) with the given key `key`. + */ +public final function Entry TakeEntry(AcediaObject key) +{ + local Entry entryToTake; + local int bucketIndex, entryIndex; + + if (!FindEntryIndices(key, bucketIndex, entryIndex)) { + return entryToTake; + } + entryToTake = hashTable[bucketIndex].entries[entryIndex]; + hashTable[bucketIndex].entries.Remove(entryIndex, 1); + storedElementCount = Max(0, storedElementCount - 1); + UpdateHashTableSize(); + return entryToTake; +} + +/** + * Returns value recorded with a given key `key` in the caller + * `HashTable`, removing it from the collection. + * + * @param key Key for which to return value. + * @param freeKey Setting this to `true` will also free the key item was + * stored with. Passed argument `key` will not be deallocated, unless it is + * the exact same object as item's key inside caller collection. + * @return Value, stored with given key `key`. If there is no value with + * such a key method will return `none`. + */ +public final function AcediaObject TakeItem(AcediaObject key) +{ + local Entry entry; + + entry = TakeEntry(key); + if (entry.key != none) { + entry.key.FreeSelf(); + } + return entry.value; +} + +/** + * Records new `value` under the key `key` into the caller `HashTable`. + * + * @param key Key by which new value will be referred to. + * @param value Value to store in the caller `HashTable`. + * @return Caller `HashTable` to allow for method chaining. + */ +public final function HashTable SetItem( + AcediaObject key, + AcediaObject value) +{ + local Entry oldEntry, newEntry; + local int bucketIndex, entryIndex; + + if (key == none) { + return self; + } + if (FindEntryIndices(key, bucketIndex, entryIndex)) { + oldEntry = hashTable[bucketIndex].entries[entryIndex]; + } + else { + storedElementCount += 1; + } + key.NewRef(); + newEntry.key = key; + newEntry.value = value; + if (value != none) { + value.NewRef(); + } + if (oldEntry.value != none && newEntry.value != oldEntry.value) { + oldEntry.value.FreeSelf(); + } + hashTable[bucketIndex].entries[entryIndex] = newEntry; + return self; +} + +/** + * Creates a new instance of class `valueClass` and records it's value with + * key `key` in the caller `HashTable`. + * + * @param key Key by which new value will be referred to. + * @param valueClass Class of object to create. Will only be created if + * passed `key` is valid. + * @return Caller `HashTable` to allow for method chaining. + */ +public final function HashTable CreateItem( + AcediaObject key, + class valueClass) +{ + local AcediaObject newItem; + + if (key == none) return self; + if (valueClass == none) return self; + + newItem = AcediaObject(_.memory.Allocate(valueClass)); + SetItem(key, newItem); + newItem.FreeSelf(); + return self; +} + +/** + * Removes a value recorded with a given key `key`. + * Does nothing if entry with a given key does not exist. + * + * @param key Key for which to remove value. + * @return Caller `HashTable` to allow for method chaining. + */ +public final function HashTable RemoveItem(AcediaObject key) +{ + local Entry entryToRemove; + local int bucketIndex, entryIndex; + + if (key == none) return self; + if (!FindEntryIndices(key, bucketIndex, entryIndex)) return self; + + entryToRemove = hashTable[bucketIndex].entries[entryIndex]; + hashTable[bucketIndex].entries.Remove(entryIndex, 1); + storedElementCount = Max(0, storedElementCount - 1); + UpdateHashTableSize(); + if (entryToRemove.value != none) { + entryToRemove.value.FreeSelf(); + } + if (entryToRemove.key != none) { + entryToRemove.key.FreeSelf(); + } + return self; +} + +public function Empty(optional bool deprecated) +{ + local int i, j; + local array nextEntries; + + for (i = 0; i < hashTable.length; i += 1) + { + nextEntries = hashTable[i].entries; + for (j = 0; j < nextEntries.length; j += 1) + { + if (nextEntries[j].value != none) { + nextEntries[j].value.FreeSelf(); + } + if (nextEntries[j].key != none) { + nextEntries[j].key.FreeSelf(); + } + } + } + hashTable.length = 0; + storedElementCount = 0; + UpdateHashTableSize(); +} + +/** + * Returns key of all properties inside caller `HashTable`. + * + * Collecting all keys from the `HashTable` is O(). + * + * See also `CopyTextKeys()` methods. + * + * @return Array of all the caller `HashTable`'s keys. + * This method does not return copies of keys, but actual keys instead - + * deallocating them will remove their item from + * the caller `HashTable`. + */ +public final function array GetKeys() +{ + local int i, j; + local array result; + local array nextEntry; + + for (i = 0; i < hashTable.length; i += 1) + { + nextEntry = hashTable[i].entries; + for (j = 0; j < nextEntry.length; j += 1) + { + nextEntry[j].key.NewRef(); + result[result.length] = nextEntry[j].key; + } + } + return result; +} + +/** + * Returns copies of `Text` key of all properties inside caller + * `HashTable`. Keys that have a different class (even if they are + * a child class for `Text`) are not returned. + * + * Collecting all keys from the `HashTable` is O(). + * + * @return Array of all the caller `HashTable`'s keys that have exactly + * `Text` class. + */ +public final function array GetTextKeys() +{ + local int i, j; + local Text nextKeyAsText; + local array result; + local array nextEntry; + + for (i = 0; i < hashTable.length; i += 1) + { + nextEntry = hashTable[i].entries; + for (j = 0; j < nextEntry.length; j += 1) + { + nextKeyAsText = Text(nextEntry[j].key); + if (nextKeyAsText != none && nextKeyAsText.class == class'Text') + { + result[result.length] = nextKeyAsText.Copy(); + } + } + } + return result; +} + +/** + * Returns amount of elements in the caller `HashTable`. + * + * Note that this value might overestimate real amount of values inside + * `HashTable` in case some of the keys used for storage were + * deallocated by code outside of `HashTable`. + * Such values might be eventually found and removed, but + * `HashTable` does not provide any guarantees on when it's done. + */ +public final function int GetLength() +{ + return storedElementCount; +} + +/** + * Auxiliary method for iterator that increments given `Index` structure. + * + * @param previousIndex Index to increment. + * @return `true` if incremented index is pointing at a valid item, + * `false` if collection has ended. + */ +public final function bool IncrementIndex(out Index previousIndex) +{ + previousIndex.entryIndex += 1; + // Go forward through buckets until we find non-empty one + while (previousIndex.bucketIndex < hashTable.length) + { + if ( previousIndex.entryIndex + < hashTable[previousIndex.bucketIndex].entries.length) + { + return true; + } + previousIndex.entryIndex = 0; + previousIndex.bucketIndex += 1; + } + return false; +} + +/** + * Auxiliary method for iterator that returns value corresponding to + * a given `Index` structure. + * + * @param index Index of item to return. + * @return Item corresponding to a given index. + * If index is invalid returns `none`. + * Note that `none` can be returned because that is simply the value + * being stored. + */ +public final function AcediaObject GetItemByIndex(Index index) +{ + local AcediaObject item; + + if (index.bucketIndex < 0) return none; + if (index.bucketIndex >= hashTable.length) return none; + + if ( index.entryIndex < 0 + || index.entryIndex >= hashTable[index.bucketIndex].entries.length) { + return none; + } + item = hashTable[index.bucketIndex].entries[index.entryIndex].value; + if (item == none) { + return none; + } + return item.NewRef(); +} + +/** + * Auxiliary method for iterator that returns value corresponding to + * a given `Index` structure. + * + * @param index Index of item to return. + * @return Key corresponding to a given index. + * If index is invalid returns `none`. + */ +public final function AcediaObject GetKeyByIndex(Index index) +{ + local AcediaObject key; + + if (index.bucketIndex < 0) return none; + if (index.bucketIndex >= hashTable.length) return none; + if ( index.entryIndex < 0 + || index.entryIndex >= hashTable[index.bucketIndex].entries.length) { + return none; + } + key = hashTable[index.bucketIndex].entries[index.entryIndex].key; + if (key == none) { + return none; + } + return key.NewRef(); +} + +protected function AcediaObject GetByText(BaseText key) +{ + return GetItem(key); +} + +/** + * Checks if value corresponding to a given `Index` structure is not `none`. + * + * @param key Key to check the value at. + * @return `true` if non-`none` value is recorded at `key` and + * `false` otherwise. + */ +public final function bool IsSomethingByIndex(Index index) +{ + if (index.bucketIndex < 0) return false; + if (index.bucketIndex >= hashTable.length) return false; + + if ( index.entryIndex < 0 + || index.entryIndex >= hashTable[index.bucketIndex].entries.length) { + return false; + } + return + (hashTable[index.bucketIndex].entries[index.entryIndex].value != none); +} + +/** + * Checks if value recorded at a given `key` is not `none`. + * + * @param key Key to check the value at. + * @return `true` if non-`none` value is recorded at `key` and + * `false` otherwise. + */ +public final function bool IsSomething(AcediaObject key) +{ + return (BorrowItem(key) != none); +} + +/** + * Returns `bool` item at key `key`. If key is invalid or + * stores a non-`bool` value, returns `defaultValue`. + * + * Referred value must be stored as `BoolBox` or `BoolRef` + * (or one of their sub-classes) for this method to work. + * + * @param key Key of a `bool` item that `HashTable` has to return. + * @param defaultValue Value to return if there is either no item recorded + * at `key` or it has a wrong type. + * @return `bool` value at `key` in the caller `HashTable`. + * `defaultValue` if passed `key` is invalid or non-`bool` value + * is stored with it. + */ +public final function bool GetBool(AcediaObject key, optional bool defaultValue) +{ + local AcediaObject result; + local BoolBox asBox; + local BoolRef asRef; + + result = BorrowItem(key); + if (result == none) { + return defaultValue; + } + asBox = BoolBox(result); + if (asBox != none) { + return asBox.Get(); + } + asRef = BoolRef(result); + if (asRef != none) { + return asRef.Get(); + } + return defaultValue; +} + +/** + * Changes `HashTable`'s value at key `key` to `value` that will be + * recorded as either `BoolBox` or `BoolRef`, depending of `asRef` + * optional parameter. + * + * @param key Key, at which to change the value. If `DynamicArray` is + * not long enough to hold it, it will be automatically expanded. + * If passed key is negative - method will do nothing. + * @param value Value to be set at a given key. + * @param asRef Given `bool` value will be recorded as immutable `BoolBox` + * by default (`asRef == false`). Setting this parameter to `true` will + * make this method record it as a mutable `BoolRef`. + * @return Reference to the caller `HashTable` to allow for + * method chaining. + */ +public final function HashTable SetBool( + AcediaObject key, + bool value, + optional bool asRef) +{ + local AcediaObject newValue; + + if (asRef) { + newValue = _.ref.bool(value); + } + else { + newValue = _.box.bool(value); + } + SetItem(key, newValue); + newValue.FreeSelf(); + return self; +} + +/** + * Returns `byte` item at key `key`. If key is invalid or + * stores a non-`byte` value, returns `defaultValue`. + * + * Referred value must be stored as `ByteBox` or `ByteBox` + * (or one of their sub-classes) for this method to work. + * + * @param key Key of a `byte` item that `HashTable` + * has to return. + * @param defaultValue Value to return if there is either no item recorded + * at `key` or it has a wrong type. + * @return `byte` value at `key` in the caller `HashTable`. + * `defaultValue` if passed `key` is invalid or non-`byte` value + * is stored with it. + */ +public final function byte GetByte(AcediaObject key, optional byte defaultValue) +{ + local AcediaObject result; + local ByteBox asBox; + local ByteRef asRef; + + result = BorrowItem(key); + if (result == none) { + return defaultValue; + } + asBox = ByteBox(result); + if (asBox != none) { + return asBox.Get(); + } + asRef = ByteRef(result); + if (asRef != none) { + return asRef.Get(); + } + return defaultValue; +} + +/** + * Changes `HashTable`'s value at key `key` to `value` that will be + * recorded as either `ByteBox` or `ByteBox`, depending of `asRef` + * optional parameter. + * + * @param key Key, at which to change the value. If `DynamicArray` is + * not long enough to hold it, it will be automatically expanded. + * If passed key is negative - method will do nothing. + * @param value Value to be set at a given key. + * @param asRef Given `byte` value will be recorded as immutable `ByteBox` + * by default (`asRef == false`). Setting this parameter to `true` will + * make this method record it as a mutable `ByteBox`. + * @return Reference to the caller `HashTable` to allow for + * method chaining. + */ +public final function HashTable SetByte( + AcediaObject key, + byte value, + optional bool asRef) +{ + local AcediaObject newValue; + + if (asRef) { + newValue = _.ref.byte(value); + } + else { + newValue = _.box.byte(value); + } + SetItem(key, newValue); + newValue.FreeSelf(); + return self; +} + +/** + * Returns `int` item at key `key`. If key is invalid or + * stores a non-`int` value, returns `defaultValue`. + * + * Referred value must be stored as `IntBox` or `IntRef` + * (or one of their sub-classes) for this method to work. + * + * @param key Key of a `int` item that `HashTable` + * has to return. + * @param defaultValue Value to return if there is either no item recorded + * at `key` or it has a wrong type. + * @return `int` value at `key` in the caller `HashTable`. + * `defaultValue` if passed `key` is invalid or non-`int` value + * is stored with it. + */ +public final function int GetInt(AcediaObject key, optional int defaultValue) +{ + local AcediaObject result; + local IntBox asBox; + local IntRef asRef; + + result = BorrowItem(key); + if (result == none) { + return defaultValue; + } + asBox = IntBox(result); + if (asBox != none) { + return asBox.Get(); + } + asRef = IntRef(result); + if (asRef != none) { + return asRef.Get(); + } + return defaultValue; +} + +/** + * Changes `HashTable`'s value at key `key` to `value` that will be + * recorded as either `IntBox` or `IntRef`, depending of `asRef` + * optional parameter. + * + * @param key Key, at which to change the value. If `DynamicArray` is + * not long enough to hold it, it will be automatically expanded. + * If passed key is negative - method will do nothing. + * @param value Value to be set at a given key. + * @param asRef Given `int` value will be recorded as immutable `IntBox` + * by default (`asRef == false`). Setting this parameter to `true` will + * make this method record it as a mutable `IntRef`. + * @return Reference to the caller `HashTable` to allow for + * method chaining. + */ +public final function HashTable SetInt( + AcediaObject key, + int value, + optional bool asRef) +{ + local AcediaObject newValue; + + if (asRef) { + newValue = _.ref.int(value); + } + else { + newValue = _.box.int(value); + } + SetItem(key, newValue); + newValue.FreeSelf(); + return self; +} + +/** + * Returns `float` item at key `key`. If key is invalid or + * stores a non-`float` value, returns `defaultValue`. + * + * Referred value must be stored as `FloatBox` or `FloatRef` + * (or one of their sub-classes) for this method to work. + * + * @param key Key of a `float` item that `HashTable` + * has to return. + * @param defaultValue Value to return if there is either no item recorded + * at `key` or it has a wrong type. + * @return `float` value at `key` in the caller `HashTable`. + * `defaultValue` if passed `key` is invalid or non-`float` value + * is stored with it. + */ +public final function float GetFloat( + AcediaObject key, + optional float defaultValue) +{ + local AcediaObject result; + local FloatBox asBox; + local FloatRef asRef; + + result = BorrowItem(key); + if (result == none) { + return defaultValue; + } + asBox = FloatBox(result); + if (asBox != none) { + return asBox.Get(); + } + asRef = FloatRef(result); + if (asRef != none) { + return asRef.Get(); + } + return defaultValue; +} + +/** + * Changes `HashTable`'s value at key `key` to `value` that will be + * recorded as either `FloatBox` or `FloatRef`, depending of `asRef` + * optional parameter. + * + * @param key Key, at which to change the value. If `DynamicArray` is + * not long enough to hold it, it will be automatically expanded. + * If passed key is negative - method will do nothing. + * @param value Value to be set at a given key. + * @param asRef Given `float` value will be recorded as immutable `FloatBox` + * by default (`asRef == false`). Setting this parameter to `true` will + * make this method record it as a mutable `FloatRef`. + * @return Reference to the caller `HashTable` to allow for + * method chaining. + */ +public final function HashTable SetFloat( + AcediaObject key, + float value, + optional bool asRef) +{ + local AcediaObject newValue; + + if (asRef) { + newValue = _.ref.float(value); + } + else { + newValue = _.box.float(value); + } + SetItem(key, newValue); + newValue.FreeSelf(); + return self; +} + +/** + * Returns `Text` item stored at key `key`. If key is invalid or + * stores a non-`Text` value, returns `none`. + * + * Referred value must be stored as `Text` (or one of it's sub-classes, + * such as `MutableText`) for this method to work. + * + * @param key Key of a `Text` item that `HashTable` + * has to return. + * @return `Text` value recorded with `key` in the caller `HashTable`. + * `none` if passed `key` is invalid or non-`Text` value + * is stored with it. + */ +public final function Text GetText(AcediaObject key) +{ + local Text result; + + result = Text(BorrowItem(key)); + if (result != none) { + result.NewRef(); + } + return result; +} + +/** + * Returns `AssociativeArray` item stored at key `key`. If key is invalid or + * stores a non-`AssociativeArray` value, returns `none`. + * + * Referred value must be stored as `AssociativeArray` + * (or one of it's sub-classes) for this method to work. + * + * @param key Key of an `AssociativeArray` item that caller `HashTable` + * has to return. + * @return `AssociativeArray` value recorded with `key` in the caller + * `HashTable`. `none` if passed `key` is invalid or + * non-`AssociativeArray` value is stored with it. + */ +public final function AssociativeArray GetAssociativeArray(AcediaObject key) +{ + local AssociativeArray result; + + result = AssociativeArray(BorrowItem(key)); + if (result != none) { + result.NewRef(); + } + return result; +} + +/** + * Returns `DynamicArray` item stored at key `key`. If key is invalid or + * stores a non-`DynamicArray` value, returns `none`. + * + * Referred value must be stored as `DynamicArray` + * (or one of it's sub-classes) for this method to work. + * + * @param key Key of a `DynamicArray` item that caller `HashTable` + * has to return. + * @return `DynamicArray` value recorded with `key` in the caller + * `HashTable`. `none` if passed `key` is invalid or + * non-`DynamicArray` value is stored with it. + */ +public final function DynamicArray GetDynamicArray(AcediaObject key) +{ + local DynamicArray result; + + result = DynamicArray(BorrowItem(key)); + if (result != none) { + result.NewRef(); + } + return result; +} + +/** + * Returns `HashTable` item stored at key `key`. If key is invalid or + * stores a non-`HashTable` value, returns `none`. + * + * Referred value must be stored as `HashTable` + * (or one of it's sub-classes) for this method to work. + * + * @param key Key of a `HashTable` item that caller `HashTable` + * has to return. + * @return `HashTable` value recorded with `key` in the caller + * `HashTable`. `none` if passed `key` is invalid or + * non-`HashTable` value is stored with it. + */ +public final function HashTable GetHashTable(AcediaObject key) +{ + local HashTable result; + + result = HashTable(BorrowItem(key)); + if (result != none) { + result.NewRef(); + } + return result; +} + +/** + * Returns `ArrayList` item stored at key `key`. If key is invalid or + * stores a non-`ArrayList` value, returns `none`. + * + * Referred value must be stored as `ArrayList` + * (or one of it's sub-classes) for this method to work. + * + * @param key Key of a `ArrayList` item that caller `HashTable` + * has to return. + * @return `ArrayList` value recorded with `key` in the caller + * `HashTable`. `none` if passed `key` is invalid or + * non-`ArrayList` value is stored with it. + */ +public final function ArrayList GetArrayList(AcediaObject key) +{ + local ArrayList result; + + result = ArrayList(BorrowItem(key)); + if (result != none) { + result.NewRef(); + } + return result; +} + +defaultproperties +{ + iteratorClass = class'HashTableIterator' + minimalCapacity = 0 + MINIMUM_SIZE = 50 + MAXIMUM_SIZE = 20000 + // `MINIMUM_DENSITY * 2 < MAXIMUM_DENSITY` must hold for `HashTable` + // to work properly + MINIMUM_DENSITY = 0.25 + MAXIMUM_DENSITY = 0.75 +} \ No newline at end of file diff --git a/sources/Data/Collections/HashTableIterator.uc b/sources/Data/Collections/HashTableIterator.uc new file mode 100644 index 0000000..397f85c --- /dev/null +++ b/sources/Data/Collections/HashTableIterator.uc @@ -0,0 +1,93 @@ +/** + * Iterator for iterating over `AssociativeArray`'s items. + * Copyright 2020 Anton Tarasenko + *------------------------------------------------------------------------------ + * This file is part of Acedia. + * + * Acedia is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3 of the License, or + * (at your option) any later version. + * + * Acedia is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with Acedia. If not, see . + */ +class HashTableIterator extends Iter + dependson(HashTable); + +var private bool hasNotFinished; +var private AssociativeArray relevantCollection; +var private AssociativeArray.Index currentIndex; + +var private bool skipNoneReferences; + +protected function Finalizer() +{ + relevantCollection = none; + skipNoneReferences = false; +} + +public function bool Initialize(Collection relevantArray) +{ + local AssociativeArray.Index emptyIndex; + + currentIndex = emptyIndex; + relevantCollection = AssociativeArray(relevantArray); + if (relevantCollection == none) { + return false; + } + hasNotFinished = (relevantCollection.GetLength() > 0); + if (GetKey() == none) { + relevantCollection.IncrementIndex(currentIndex); + } + return true; +} + +public function LeaveOnlyNotNone() +{ + skipNoneReferences = true; +} + +public function Iter Next(optional bool deprecated) +{ + local int collectionLength; + + if (!skipNoneReferences) + { + hasNotFinished = relevantCollection.IncrementIndex(currentIndex); + return self; + } + collectionLength = relevantCollection.GetLength(); + while (hasNotFinished) + { + hasNotFinished = relevantCollection.IncrementIndex(currentIndex); + if (relevantCollection.GetEntryByIndex(currentIndex).value != none) { + return self; + } + } + return self; +} + +public function AcediaObject Get() +{ + return relevantCollection.GetEntryByIndex(currentIndex).value; +} + +public function AcediaObject GetKey() +{ + return relevantCollection.GetEntryByIndex(currentIndex).key; +} + +public function bool HasFinished() +{ + return !hasNotFinished; +} + +defaultproperties +{ +} \ No newline at end of file