Browse Source

Change `JObject` to use hash tables and classes

pull/8/head
Anton Tarasenko 4 years ago
parent
commit
c0b719a2f2
  1. 430
      sources/Data/JSON/JObject.uc
  2. 23
      sources/Data/JSON/JSON.uc
  3. 171
      sources/Data/JSON/Tests/TEST_JSON.uc

430
sources/Data/JSON/JObject.uc

@ -42,21 +42,170 @@ struct JProperty
var string name;
var JStorageAtom value;
};
var private array<JProperty> properties;
// Bucket of alias-value pairs, with the same alias hash.
struct PropertyBucket
{
var array<JProperty> properties;
};
var private array<PropertyBucket> hashTable;
var private int storedElementCount;
// Returns index of name-value pair in `properties` for a given name.
// Returns `-1` if such a pair does not exist.
private final function int GetPropertyIndex(string name)
// 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;
var private config const float MINIMUM_DENSITY;
var private config const float MAXIMUM_DENSITY;
var private config const int MINIMUM_DIFFERENCE_FOR_REALLOCATION;
var private config const int ABSOLUTE_LOWER_CAPACITY_LIMIT;
// 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
// (`propertyIndex`).
// `bucketIndex` is always found,
// `propertyIndex` is valid iff method returns `true`, otherwise it's equal to
// the index at which new property can get inserted.
private final function bool FindPropertyIndices(
string name,
out int bucketIndex,
out int propertyIndex)
{
local int i;
for (i = 0; i < properties.length; i += 1)
local array<JProperty> bucketProperties;
TouchHashTable();
bucketIndex = _.text.GetHash(name);
if (bucketIndex < 0) {
bucketIndex *= -1;
}
bucketIndex = Remainder(bucketIndex, hashTable.length);
// Check if bucket actually has given name.
bucketProperties = hashTable[bucketIndex].properties;
for (i = 0; i < bucketProperties.length; i += 1)
{
if (bucketProperties[i].name == name)
{
if (name == properties[i].name)
propertyIndex = i;
return true;
}
}
propertyIndex = bucketProperties.length;
return false;
}
// Creates hash table in case it does not exist yet
private final function TouchHashTable()
{
if (hashTable.length <= 0) {
UpdateHashTableCapacity();
}
}
// Attempts to find a property in a caller `JObject` by the name `name` and
// writes it into `result`. Returns `true` if it succeeds and `false` otherwise
// (in that case writes a blank property with a given name in `result`).
private final function bool FindProperty(string name, out JProperty result)
{
local JProperty newProperty;
local int bucketIndex, propertyIndex;
if (FindPropertyIndices(name, bucketIndex, propertyIndex))
{
result = hashTable[bucketIndex].properties[propertyIndex];
return true;
}
newProperty.name = name;
result = newProperty;
return false;
}
// Creates/replaces a property with a name `newProperty.name` in caller
// JSON object
private final function UpdateProperty(JProperty newProperty)
{
local bool overriddenProperty;
local int bucketIndex, propertyIndex;
overriddenProperty = !FindPropertyIndices( newProperty.name,
bucketIndex, propertyIndex);
hashTable[bucketIndex].properties[propertyIndex] = newProperty;
if (overriddenProperty) {
storedElementCount += 1;
UpdateHashTableCapacity();
}
}
// Removes a property with a name `newProperty.name` from caller JSON object
// Returns `true` if something was actually removed.
private final function bool RemoveProperty(string propertyName)
{
local int bucketIndex, propertyIndex;
// Ensure has table was initialized before any updates
if (hashTable.length <= 0) {
UpdateHashTableCapacity();
}
if (FindPropertyIndices(propertyName, bucketIndex, propertyIndex)) {
hashTable[bucketIndex].properties.Remove(propertyIndex, 1);
storedElementCount = Max(0, storedElementCount - 1);
UpdateHashTableCapacity();
return true;
}
return false;
}
// Checks if we need to change our current capacity and does so if needed
private final function UpdateHashTableCapacity()
{
local int oldCapacity, newCapacity;
oldCapacity = hashTable.length;
// Calculate new capacity (and whether it is needed) based on amount of
// stored properties and current capacity
newCapacity = oldCapacity;
if (storedElementCount < newCapacity * MINIMUM_DENSITY) {
newCapacity /= 2;
}
if (storedElementCount > newCapacity * MAXIMUM_DENSITY) {
newCapacity *= 2;
}
// Enforce our limits
newCapacity = Clamp(newCapacity, MINIMUM_CAPACITY, MAXIMUM_CAPACITY);
newCapacity = Max(ABSOLUTE_LOWER_CAPACITY_LIMIT, newCapacity);
// Only resize if difference is huge enough or table does not exists yet
if ( newCapacity - oldCapacity > MINIMUM_DIFFERENCE_FOR_REALLOCATION
|| oldCapacity - newCapacity > MINIMUM_DIFFERENCE_FOR_REALLOCATION
|| oldCapacity <= 0) {
ResizeHashTable(newCapacity);
}
}
// Change size of the hash table, does not check any limits, does not check
// if `newCapacity` is a valid capacity (`newCapacity > 0`).
// Use `UpdateHashTableCapacity()` for that.
private final function ResizeHashTable(int newCapacity)
{
local int i, j;
local array<JProperty> bucketProperties;
local array<PropertyBucket> oldHashTable;
oldHashTable = hashTable;
// Clean current hash table
hashTable.length = 0;
hashTable.length = newCapacity;
for (i = 0; i < oldHashTable.length; i += 1)
{
return i;
bucketProperties = oldHashTable[i].properties;
for (j = 0; j < bucketProperties.length; j += 1) {
UpdateProperty(bucketProperties[j]);
}
}
return -1;
}
// Returns `JType` of a variable with a given name in our properties.
@ -65,11 +214,11 @@ private final function int GetPropertyIndex(string name)
// function will return `JSON_Undefined`.
public final function JType GetTypeOf(string name)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return JSON_Undefined;
return properties[index].value.type;
local JProperty property;
FindProperty(name, property);
// If we did not find anything - `property` will be set up as
// a `JSON_Undefined` type value.
return property.value.type;
}
// Following functions are getters for various types of variables.
@ -82,66 +231,93 @@ public final function JType GetTypeOf(string name)
// will simply return `none`.
public final function float GetNumber(string name, optional float defaultValue)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return defaultValue;
if (properties[index].value.type != JSON_Number) return defaultValue;
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Number) {
return defaultValue;
}
return property.value.numberValue;
}
return properties[index].value.numberValue;
public final function int GetInteger(string name, optional int defaultValue)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Number) {
return defaultValue;
}
return property.value.numberValueAsInt;
}
public final function string GetString
(
public final function string GetString(
string name,
optional string defaultValue
)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return defaultValue;
if (properties[index].value.type != JSON_String) return defaultValue;
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_String) {
return defaultValue;
}
return property.value.stringValue;
}
return properties[index].value.stringValue;
public final function class<Object> GetClass(
string name,
optional class<Object> defaultValue
)
{
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_String) {
return defaultValue;
}
if (!property.value.classLoadingWasAttempted)
{
property.value.classLoadingWasAttempted = true;
property.value.stringValueAsClass =
class<Object>(DynamicLoadObject( property.value.stringValue,
class'Class', true));
}
if (property.value.stringValueAsClass != none) {
return property.value.stringValueAsClass;
}
return defaultValue;
}
public final function bool GetBoolean(string name, optional bool defaultValue)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return defaultValue;
if (properties[index].value.type != JSON_Boolean) return defaultValue;
return properties[index].value.booleanValue;
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Boolean) {
return defaultValue;
}
return property.value.booleanValue;
}
public final function bool IsNull(string name)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return false;
if (properties[index].value.type != JSON_Null) return false;
return (properties[index].value.type == JSON_Null);
local JProperty property;
FindProperty(name, property);
return (property.value.type == JSON_Null);
}
public final function JArray GetArray(string name)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return none;
if (properties[index].value.type != JSON_Array) return none;
return JArray(properties[index].value.complexValue);
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Array) {
return none;
}
return JArray(property.value.complexValue);
}
public final function JObject GetObject(string name)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return none;
if (properties[index].value.type != JSON_Object) return none;
return JObject(properties[index].value.complexValue);
local JProperty property;
FindProperty(name, property);
if (property.value.type != JSON_Object) return none;
return JObject(property.value.complexValue);
}
// Following functions provide simple setters for boolean, string, number
@ -150,65 +326,72 @@ public final function JObject GetObject(string name)
// `object.SetNumber("num1", 1).SetNumber("num2", 2);`.
public final function JObject SetNumber(string name, float value)
{
local int index;
local JProperty newProperty;
index = GetPropertyIndex(name);
if (index < 0)
{
index = properties.length;
}
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Number;
property.value.numberValue = value;
property.value.numberValueAsInt = int(value);
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
newProperty.name = name;
newProperty.value.type = JSON_Number;
newProperty.value.numberValue = value;
properties[index] = newProperty;
public final function JObject SetInteger(string name, int value)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Number;
property.value.numberValue = float(value);
property.value.numberValueAsInt = value;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
public final function JObject SetString(string name, string value)
{
local int index;
local JProperty newProperty;
index = GetPropertyIndex(name);
if (index < 0)
{
index = properties.length;
}
newProperty.name = name;
newProperty.value.type = JSON_String;
newProperty.value.stringValue = value;
properties[index] = newProperty;
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_String;
property.value.stringValue = value;
property.value.stringValueAsClass = none;
property.value.classLoadingWasAttempted = false;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
public final function JObject SetClass(string name, class<Object> value)
{
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_String;
property.value.stringValue = string(value);
property.value.stringValueAsClass = value;
property.value.classLoadingWasAttempted = true;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
public final function JObject SetBoolean(string name, bool value)
{
local int index;
local JProperty newProperty;
index = GetPropertyIndex(name);
if (index < 0)
{
index = properties.length;
}
newProperty.name = name;
newProperty.value.type = JSON_Boolean;
newProperty.value.booleanValue = value;
properties[index] = newProperty;
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Boolean;
property.value.booleanValue = value;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
public final function JObject SetNull(string name)
{
local int index;
local JProperty newProperty;
index = GetPropertyIndex(name);
if (index < 0)
{
index = properties.length;
}
newProperty.name = name;
newProperty.value.type = JSON_Null;
properties[index] = newProperty;
local JProperty property;
FindProperty(name, property);
property.value.type = JSON_Null;
property.value.complexValue = none;
UpdateProperty(property);
return self;
}
@ -218,48 +401,59 @@ public final function JObject SetNull(string name)
// `object.CreateObject("folded object").CreateArray("names list");`.
public final function JObject CreateArray(string name)
{
local int index;
local JProperty newProperty;
index = GetPropertyIndex(name);
if (index < 0)
{
index = properties.length;
local JProperty property;
FindProperty(name, property);
if (property.value.complexValue != none) {
property.value.complexValue.Destroy();
}
newProperty.name = name;
newProperty.value.type = JSON_Array;
newProperty.value.complexValue = _.json.newArray();
properties[index] = newProperty;
property.value.type = JSON_Array;
property.value.complexValue = _.json.NewArray();
UpdateProperty(property);
return self;
}
public final function JObject CreateObject(string name)
{
local int index;
local JProperty newProperty;
index = GetPropertyIndex(name);
if (index < 0)
{
index = properties.length;
local JProperty property;
FindProperty(name, property);
if (property.value.complexValue != none) {
property.value.complexValue.Destroy();
}
newProperty.name = name;
newProperty.value.type = JSON_Object;
newProperty.value.complexValue = _.json.newObject();
properties[index] = newProperty;
property.value.type = JSON_Object;
property.value.complexValue = _.json.NewObject();
UpdateProperty(property);
return self;
}
// Removes values with a given name.
// Returns `true` if value was actually removed and `false` if it didn't exist.
public final function bool RemoveValue(string name)
public final function JObject RemoveValue(string name)
{
local int index;
index = GetPropertyIndex(name);
if (index < 0) return false;
RemoveProperty(name);
return self;
}
properties.Remove(index, 1);
return true;
public final function array<string> GetKeys()
{
local int i, j;
local array<string> result;
local array<JProperty> nextProperties;
for (i = 0; i < hashTable.length; i += 1)
{
nextProperties = hashTable[i].properties;
for (j = 0; j < nextProperties.length; j += 1) {
result[result.length] = nextProperties[j].name;
}
}
return result;
}
defaultproperties
{
ABSOLUTE_LOWER_CAPACITY_LIMIT = 10
MINIMUM_CAPACITY = 50
MAXIMUM_CAPACITY = 100000
MINIMUM_DENSITY = 0.25
MAXIMUM_DENSITY = 0.75
MINIMUM_DIFFERENCE_FOR_REALLOCATION = 50
}

23
sources/Data/JSON/JSON.uc

@ -64,21 +64,18 @@ struct JStorageAtom
var protected bool booleanValue;
// Used for storing both JSON objects and arrays.
var protected JSON complexValue;
// Numeric value might not fit into a `float` very well, so we will store
// them as both `float` and `integer` and allow user to request any version
// of them
var protected int numberValueAsInt;
// Some `string` values might be actually used to represent classes,
// so we will give users an ability to request `string` value as a class.
var protected class<Object> stringValueAsClass;
// To avoid several unsuccessful attempts to load `class` object from
// a `string`, we will record whether we've already tied that.
var protected bool classLoadingWasAttempted;
};
// TODO: Rewrite JSON object to use more efficient storage data structures
// that will support subtypes:
// ~ Number: byte, int, float
// ~ String: string, class
// (maybe move to auto generated code?).
// TODO: Add cleanup queue to efficiently and without crashes clean up
// removed objects.
// TODO: Add `JValue` - a reference type for number / string / boolean / null
// TODO: Add accessors for last values.
// TODO: Add path-getters.
// TODO: Add iterators.
// TODO: Add parsing/printing.
// TODO: Add functions for deep copy.
defaultproperties
{
}

171
sources/Data/JSON/Tests/TEST_JSON.uc

@ -26,6 +26,7 @@ protected static function TESTS()
local JObject jsonData;
jsonData = _().json.newObject();
Test_ObjectGetSetRemove();
Test_ObjectKeys();
Test_ArrayGetSetRemove();
}
@ -33,8 +34,12 @@ protected static function Test_ObjectGetSetRemove()
{
SubTest_Undefined();
SubTest_StringGetSetRemove();
SubTest_ClassGetSetRemove();
SubTest_StringAsClass();
SubTest_BooleanGetSetRemove();
SubTest_NumberGetSetRemove();
SubTest_IntegerGetSetRemove();
SubTest_FloatAndInteger();
SubTest_NullGetSetRemove();
SubTest_MultipleVariablesGetSet();
SubTest_Object();
@ -141,6 +146,72 @@ protected static function SubTest_StringGetSetRemove()
TEST_ExpectTrue(testJSON.GetString("some_string", "other") == "other");
}
protected static function SubTest_ClassGetSetRemove()
{
local JObject testJSON;
testJSON = _().json.newObject();
testJSON.SetClass("info_class", class'Info');
Context("Testing `JObject`'s get/set/remove functions for" @
"class variables");
Issue("String type isn't properly set by `SetClass`");
TEST_ExpectTrue(testJSON.GetTypeOf("info_class") == JSON_String);
Issue("Value is incorrectly assigned by `SetClass`");
TEST_ExpectTrue(testJSON.GetClass("info_class") == class'Info');
Issue( "Providing default variable value makes 'GetClass'" @
"return wrong value");
TEST_ExpectTrue( testJSON.GetClass("info_class", class'Actor')
== class'Info');
Issue("Variable value isn't correctly reassigned by `SetClass`");
testJSON.SetClass("info_class", class'ReplicationInfo');
TEST_ExpectTrue(testJSON.GetClass("info_class") == class'ReplicationInfo');
Issue( "Getting class variable as a wrong type" @
"doesn't yield default value");
TEST_ExpectTrue(testJSON.GetBoolean("info_class", true) == true);
Issue("Class variable isn't being properly removed");
testJSON.RemoveValue("info_class");
TEST_ExpectTrue(testJSON.GetTypeOf("info_class") == JSON_Undefined);
Issue( "Getters don't return default value for missing key that" @
"previously stored class value, but got removed");
TEST_ExpectTrue( testJSON.GetClass("info_class", class'Actor')
== class'Actor');
}
protected static function SubTest_StringAsClass()
{
local JObject testJSON;
testJSON = _().json.newObject();
testJSON.SetString("SetString", "Engine.Actor");
testJSON.SetString("SetStringIncorrect", "blahblahblah");
testJSON.SetClass("SetClass", class'Info');
testJSON.SetClass("none", none);
Context("Testing how `JObject` treats mixed string and"
@ "class setters/getters.");
Issue("Incorrect result of `SetClass().GetString()` sequence.");
TEST_ExpectTrue(testJSON.GetString("SetClass") == "Engine.Info");
TEST_ExpectTrue(testJSON.GetString("none") == "None");
TEST_ExpectTrue(testJSON.GetString("none", "alternative") == "None");
Issue("Incorrect result of `SetString().GetClass()` sequence for"
@ "correct value in `SetString()`.");
TEST_ExpectTrue(testJSON.GetClass("SetString") == class'Actor');
TEST_ExpectTrue( testJSON.GetClass("SetString", class'Object')
== class'Actor');
Issue("Incorrect result of `SetString().GetClass()` sequence for"
@ "incorrect value in `SetString()`.");
TEST_ExpectTrue(testJSON.GetClass("SetStringIncorrect") == none);
TEST_ExpectTrue( testJSON.GetClass("SetStringIncorrect", class'Object')
== class'Object');
}
protected static function SubTest_NumberGetSetRemove()
{
local JObject testJSON;
@ -148,7 +219,7 @@ protected static function SubTest_NumberGetSetRemove()
testJSON.SetNumber("some_number", 3.5);
Context("Testing `JObject`'s get/set/remove functions for" @
"number variables");
"number variables as floats");
Issue("Number type isn't properly set by `SetNumber`");
TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Number);
@ -176,6 +247,64 @@ protected static function SubTest_NumberGetSetRemove()
TEST_ExpectTrue(testJSON.GetNumber("some_number", 13) == 13);
}
protected static function SubTest_IntegerGetSetRemove()
{
local JObject testJSON;
testJSON = _().json.newObject();
testJSON.SetInteger("some_number", 33653);
Context("Testing `JObject`'s get/set/remove functions for" @
"number variables as integers");
Issue("Number type isn't properly set by `SetInteger`");
TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Number);
Issue("Value is incorrectly assigned by `SetInteger`");
TEST_ExpectTrue(testJSON.GetInteger("some_number") == 33653);
Issue( "Providing default variable value makes 'GetInteger'" @
"return wrong value");
TEST_ExpectTrue(testJSON.GetInteger("some_number", 5) == 33653);
Issue("Variable value isn't correctly reassigned by `SetInteger`");
testJSON.SetInteger("some_number", MaxInt);
TEST_ExpectTrue(testJSON.GetInteger("some_number") == MaxInt);
Issue( "Getting number variable as a wrong type" @
"doesn't yield default value.");
TEST_ExpectTrue(testJSON.GetString("some_number", "default") == "default");
Issue("Number type isn't being properly removed");
testJSON.RemoveValue("some_number");
TEST_ExpectTrue(testJSON.GetTypeOf("some_number") == JSON_Undefined);
Issue( "Getters don't return default value for missing key that" @
"previously stored number value, that got removed");
TEST_ExpectTrue(testJSON.GetInteger("some_number", -235) == -235);
}
protected static function SubTest_FloatAndInteger()
{
local JObject testJSON;
testJSON = _().json.newObject();
testJSON.SetNumber("SetNumber", 6.70087);
testJSON.SetInteger("SetInteger", 62478623874);
Context("Testing how `JObject` treats mixed float and"
@ "integer setters/getters.");
Issue("Incorrect result of `SetNumber().GetInteger()` sequence.");
TEST_ExpectTrue(testJSON.GetInteger("SetNumber") == 6);
testJSON.SetInteger("SetNumber", 11);
testJSON.SetNumber("SetInteger", 0.43);
Issue("SetNumber().SetInteger() for same variable name does not overwrite"
@ "initial number value.");
TEST_ExpectTrue(testJSON.GetNumber("SetNumber") == 11);
Issue("SetInteger().SetNumber() for same variable name does not overwrite"
@ "initial integer value.");
TEST_ExpectTrue(testJSON.GetInteger("SetInteger") == 0);
}
protected static function SubTest_NullGetSetRemove()
{
local JObject testJSON;
@ -284,6 +413,46 @@ protected static function SubTest_Object()
.GetString("in", "default") == "string inside");
}
protected static function Test_ObjectKeys()
{
local int i;
local bool varFound, clsFound, objFound;
local JObject testObject;
local array<string> keys;
testObject = _().json.newObject();
Context("Testing getting list of keys from the `JObject`.");
Issue("Just created `JObject` returns non-empty key list.");
TEST_ExpectTrue(testObject.GetKeys().length == 0);
Issue("`JObject` returns incorrect key list.");
keys = testObject.SetInteger("var", 7).SetClass("cls", class'Actor')
.CreateObject("obj").GetKeys();
TEST_ExpectTrue(keys.length == 3);
for (i = 0; i < keys.length; i += 1)
{
if (keys[i] == "var") { varFound = true; }
if (keys[i] == "cls") { clsFound = true; }
if (keys[i] == "obj") { objFound = true; }
}
TEST_ExpectTrue(varFound && clsFound && objFound);
Issue("`JObject` returns incorrect key list after removing an element.");
keys = testObject.RemoveValue("cls").GetKeys();
TEST_ExpectTrue(keys.length == 2);
varFound = false;
objFound = false;
for (i = 0; i < keys.length; i += 1)
{
if (keys[i] == "var") { varFound = true; }
if (keys[i] == "obj") { objFound = true; }
}
TEST_ExpectTrue(varFound && objFound);
Issue("`JObject` returns incorrect key list after removing all elements.");
keys = testObject.RemoveValue("var").RemoveValue("obj").GetKeys();
TEST_ExpectTrue(keys.length == 0);
}
protected static function SubTest_ArrayUndefined()
{
local JArray testJSON;

Loading…
Cancel
Save