/**
* A class, implementing a hash-table-based dictionary for quick access to
* aliases' values.
* It does not support dynamic hash table capacity change and
* requires to set the size upfront.
* 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 AliasHash extends AcediaObject
dependson(AliasSource)
config(AcediaSystem);
// Reasonable lower and upper limits on hash table capacity,
// that will be enforced if user requires something outside those bounds
var private config const int MINIMUM_CAPACITY;
var private config const int MAXIMUM_CAPACITY;
// Bucket of alias-value pairs, with the same alias hash.
struct PairBucket
{
var array pairs;
};
var private array hashTable;
/**
* Initializes caller `AliasHash`.
*
* Calling this function again will clear all existing data and will create
* a brand new hash table.
*
* @param desiredCapacity Desired capacity of the underlying hash table.
* Will be clamped between `MINIMUM_CAPACITY` and `MAXIMUM_CAPACITY`.
* Not specifying anything as this parameter creates a hash table of
* size `MINIMUM_CAPACITY`.
* @return A reference to a caller object to allow for function chaining.
*/
public final function AliasHash Initialize(optional int desiredCapacity)
{
desiredCapacity = Clamp(desiredCapacity, MINIMUM_CAPACITY,
MAXIMUM_CAPACITY);
hashTable.length = 0;
hashTable.length = desiredCapacity;
return self;
}
// Helper method that is needed as a replacement for `%`, since it is
// an operation on `float`s in UnrealScript and does not have enough precision
// to work with hashes.
// Assumes positive input.
private function int Remainder(int number, int divisor)
{
local int quotient;
quotient = number / divisor;
return (number - quotient * divisor);
}
// Finds indices for:
// 1. Bucked that contains specified alias (`bucketIndex`);
// 2. Pair for specified alias in the bucket's collection (`pairIndex`).
// `bucketIndex` is always found,
// `pairIndex` is valid iff method returns `true`.
private final function bool FindPairIndices(
string alias,
out int bucketIndex,
out int pairIndex)
{
local int i;
local array bucketPairs;
// `Locs()` is used because aliases are case-insensitive.
bucketIndex = _().text.GetHash(Locs(alias));
if (bucketIndex < 0) {
bucketIndex *= -1;
}
bucketIndex = Remainder(bucketIndex, hashTable.length);
// Check if bucket actually has given alias.
bucketPairs = hashTable[bucketIndex].pairs;
for (i = 0; i < bucketPairs.length; i += 1)
{
if (bucketPairs[i].alias ~= alias)
{
pairIndex = i;
return true;
}
}
return false;
}
/**
* Finds a value for a given alias.
*
* @param alias Alias for which we need to find a value.
* Aliases are case-insensitive.
* @param value If given alias is present in caller `AliasHash`, -
* it's value will be written in this variable.
* Otherwise value is undefined.
* @return `true` if we found value, `false` otherwise.
*/
public final function bool Find(string alias, out string value)
{
local int bucketIndex;
local int pairIndex;
if (FindPairIndices(alias, bucketIndex, pairIndex))
{
value = hashTable[bucketIndex].pairs[pairIndex].value;
return true;
}
return false;
}
/**
* Checks if caller `AliasHash` contains given alias.
*
* @param alias Alias to check for belonging to caller `AliasHash`.
* Aliases are case-insensitive.
* @return `true` if caller `AliasHash` contains the value for a given alias
* and `false` otherwise.
*/
public final function bool Contains(string alias)
{
local int bucketIndex;
local int pairIndex;
return FindPairIndices(alias, bucketIndex, pairIndex);
}
/**
* Inserts new record for alias `alias` for value of `value`.
*
* If there is already a value for a given `alias` - it will be overwritten.
*
* @param alias Alias to insert. Aliases are case-insensitive.
* @param value Value for a given alias to store.
* @return A reference to a caller object to allow for function chaining.
*/
public final function AliasHash Insert(string alias, string value)
{
local int bucketIndex;
local int pairIndex;
local AliasSource.AliasValuePair newRecord;
newRecord.value = value;
newRecord.alias = alias;
if (!FindPairIndices(alias, bucketIndex, pairIndex)) {
pairIndex = hashTable[bucketIndex].pairs.length;
}
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
return self;
}
/**
* Inserts new record for alias `alias` for value of `value`.
*
* If there is already a value for a given `alias`, - new value will be
* discarded and `AliasHash` will not be changed.
*
* @param alias Alias to insert. Aliases are case-insensitive.
* @param value Value for a given alias to store.
* @param existingValue Value that will correspond to a given alias after
* this method's execution. If insertion was successful - given `value`,
* otherwise (if there already was a record for an `alias`)
* it will return value that already existed in caller `AliasHash`.
* @return `true` if given alias-value pair was inserted and `false` otherwise.
*/
public final function bool InsertIfMissing(
string alias,
string value,
out string existingValue)
{
local int bucketIndex;
local int pairIndex;
local AliasSource.AliasValuePair newRecord;
newRecord.value = value;
newRecord.alias = alias;
existingValue = value;
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
existingValue = hashTable[bucketIndex].pairs[pairIndex].value;
return false;
}
pairIndex = hashTable[bucketIndex].pairs.length;
hashTable[bucketIndex].pairs[pairIndex] = newRecord;
return true;
}
/**
* Removes record, corresponding to a given alias `alias`.
*
* @param alias Alias for which all records must be removed.
* @return `true` if record was removed, `false` if id did not
* (can only happen when `AliasHash` did not have any records for `alias`).
*/
public final function bool Remove(string alias)
{
local int bucketIndex;
local int pairIndex;
if (FindPairIndices(alias, bucketIndex, pairIndex)) {
hashTable[bucketIndex].pairs.Remove(pairIndex, 1);
return true;
}
return false;
}
defaultproperties
{
MINIMUM_CAPACITY = 10
MAXIMUM_CAPACITY = 100000
}