diff --git a/sources/Text/JSON/JSONAPI.uc b/sources/Text/JSON/JSONAPI.uc index 3c425e2..04a2674 100644 --- a/sources/Text/JSON/JSONAPI.uc +++ b/sources/Text/JSON/JSONAPI.uc @@ -5,7 +5,7 @@ * both valid and invalid JSON. However only correctly parsing valid JSON * is guaranteed. This means that you should not rely on these methods to parse * any JSON extensions or validate JSON for you. - * Copyright 2021-2022 Anton Tarasenko + * Copyright 2021-2023 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * @@ -1381,6 +1381,228 @@ private final function int GetEscapedVersion(int codePoint) return codePoint; } +/** + * Increments given value by another value, producing result as a new + * JSON-compatible value. + * + * What "incrementing" actually does depends on the passed JSON values: + * `valueToIncrement` and `increment` parameters. Unless either of them is `none` + * (then "increment" simply acts as a `_.json.Copy()` method for + * the non-`none` one), they must represent the same JSON type and: + * + * 1. JSON bool: performs logical "or" operation on given values; + * 2. JSON number: adds values together; + * 3. JSON string: appends `increment` at the end of `valueToIncrement`; + * 4. JSON array: appends copy of elements of `increment` at the end of + * array of copies of elements of `valueToIncrement` + * (calls `ArrayList::append()` method); + * 5. JSON object: creates new collection, based on `valueToIncrement` + * with added key-value pairs from `increment` (all elements are + * copied). Does not override old values + * (calls `HashTable::append()` method). + * + * In case they represent different JSON types (that aren't "null") - + * incrementing should produce `none`. + * + * @param valueToIncrement Value to increment. Can be any JSON-compatible + * value. + * @param increment Value to increment it by. Can be any + * JSON-compatible value. + * @return Incremented data (guaranteed to contain copies and not actual + * objects from either `valueToIncrement` or `increment`). `none` if + * argument types were incompatible. Whether type of the result will be + * immutable (boxes and `Text`) or mutable (refs and `MutableText`) + * depends on immutability of `valueToIncrement`. When adding two numbers, + * whether result will be boxed (this includes both boxes and refs) `int` + * or `float` depends on both parameters - if either of them if `float`, + * then result will be `float`. + */ +public final function AcediaObject Increment( + AcediaObject valueToIncrement, + AcediaObject increment) +{ + local AcediaObject result; + + if (valueToIncrement == none) { + result = _.json.Copy(increment); + } + else if (increment == none) { + result = _.json.Copy(valueToIncrement); + } + else if ( valueToIncrement.class == class'IntBox' + || valueToIncrement.class == class'IntRef' + || valueToIncrement.class == class'FloatBox' + || valueToIncrement.class == class'FloatRef') + { + result = Increment_Number(valueToIncrement, increment); + } + else if ( valueToIncrement.class == class'BoolBox' + || valueToIncrement.class == class'BoolRef') + { + result = Increment_Bool(valueToIncrement, increment); + } + else if ( valueToIncrement.class == class'Text' + || valueToIncrement.class == class'MutableText') + { + result = Increment_Text(valueToIncrement, increment); + } + else { + result = Increment_Collections(valueToIncrement, increment); + } + return result; +} + +// Assumes `valueToIncrement` and `increment` aren't `none` +private final function AcediaObject Increment_Collections( + AcediaObject valueToIncrement, + AcediaObject increment) +{ + local AcediaObject result; + local ArrayList arrayListCopy; + local HashTable hashTableCopy; + + if ( valueToIncrement.class == class'ArrayList' + && increment.class == class'ArrayList') + { + arrayListCopy = ArrayList(Copy(ArrayList(increment))); + result = CopyArrayList(ArrayList(valueToIncrement)) + .Append(arrayListCopy); + _.memory.Free(arrayListCopy); + } + else if ( valueToIncrement.class == class'HashTable' + && increment.class == class'HashTable') + { + hashTableCopy = HashTable(Copy(HashTable(increment))); + result = CopyHashTable(HashTable(valueToIncrement)) + .Append(hashTableCopy); + _.memory.Free(hashTableCopy); + } + return result; +} + +// Assumes `valueToIncrement` and `increment` aren't `none` +private final function AcediaObject Increment_Text( + AcediaObject valueToIncrement, + AcediaObject increment) +{ + local BaseText textIncrement; + local MutableText builder; + + textIncrement = BaseText(increment); + if (BaseText(increment) == none) { + return none; + } + builder = BaseText(valueToIncrement).MutableCopy(); + builder.Append(textIncrement); + if (Text(valueToIncrement) != none) { + return builder.IntoText(); + } + return builder; +} + +// Assumes `valueToIncrement` and `increment` aren't `none` +private final function AcediaObject Increment_Bool( + AcediaObject valueToIncrement, + AcediaObject increment) +{ + local bool value1, value2; + + if (valueToIncrement.class == class'BoolBox') { + value1 = BoolBox(valueToIncrement).Get(); + } + if (valueToIncrement.class == class'BoolRef') { + value1 = BoolRef(valueToIncrement).Get(); + } + if (increment.class == class'BoolBox') { + value2 = BoolBox(increment).Get(); + } + else if (increment.class == class'BoolRef') { + value2 = BoolRef(increment).Get(); + } + else { + return none; + } + if (ValueBox(valueToIncrement) != none) { + return _.box.bool(value1 || value2); + } + return _.ref.bool(value1 || value2); +} + +// Assumes `valueToIncrement` and `increment` aren't `none` +// Assumes `valueToIncrement` is one of four classes: `IntBox`, `IntRef`, +// `FloatBox` or `FloatRef`. +private final function AcediaObject Increment_Number( + AcediaObject valueToIncrement, + AcediaObject increment) +{ + local bool hasFloats; + local int intSummand1, intSummand2, intSum; + local float floatSummand1, floatSummand2, floatSum; + + hasFloats = valueToIncrement.class == class'FloatBox' + || valueToIncrement.class == class'FloatRef' + || increment.class == class'FloatBox' + || increment.class == class'FloatRef'; + // `valueToIncrement` is guaranteed to have an appropriate type, + // but `increment` might not, so only do check on second call + ExtractBoxedNumericValue(valueToIncrement, intSummand1, floatSummand1); + if (!ExtractBoxedNumericValue(increment, intSummand2, floatSummand2)) { + return none; + } + if (hasFloats) + { + floatSum = floatSummand1 + floatSummand2 + + float(intSummand1 + intSummand2); + if (ValueBox(valueToIncrement) != none) { + return _.box.float(floatSum); + } + return _.ref.float(floatSum); + } + intSum = intSummand1 + intSummand2 + int(floatSummand1 + floatSummand2); + if (ValueBox(valueToIncrement) != none) { + return _.box.int(intSum); + } + return _.ref.int(intSum); +} + +// Extracts numeric value and records it into one of two out arguments: +// +// * `asInteger` iff `value` is either `IntBox` or `IntRef`; +// * `asFloat` iff `value` is either `FloatBox` or `FloatRef`; +// +// Does not change the value in remaining parameter. +// Returns `true` in case of success (method managed to read the value) and +// `false` otherwise (`value` had non-numeric or unknown type). +private final function bool ExtractBoxedNumericValue( + AcediaObject value, + out int asInteger, + out float asFloat) +{ + local bool success; + + if (value.class == class'IntBox') + { + asInteger = IntBox(value).Get(); + success = true; + } + else if (value.class == class'IntRef') + { + asInteger = IntRef(value).Get(); + success = true; + } + else if (value.class == class'FloatBox') + { + asFloat = FloatBox(value).Get(); + success = true; + } + else if (value.class == class'FloatRef') + { + asFloat = FloatRef(value).Get(); + success = true; + } + return success; +} + /** * Performs a deep copy of the `inputData`. This means it copies not only * `inputData` itself, but (in case it is a container) all of the values