Browse Source

Add initial `BigInt` version

pull/8/head
Anton Tarasenko 2 years ago
parent
commit
f85d2fb3c3
  1. 280
      sources/Data/Database/BigInt.uc
  2. 106
      sources/Data/Database/Tests/TEST_BigInt.uc
  3. 11
      sources/Manifest.uc

280
sources/Data/Database/BigInt.uc

@ -0,0 +1,280 @@
/**
* A simply big integer implementation, mostly to allow Acedia's databases to
* store integers of arbitrary size. Should not be used in regular
* computations, designed to store player statistic values that are incremented
* from time to time.
* 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 <https://www.gnu.org/licenses/>.
*/
class BigInt extends AcediaObject
dependson(MathAPI);
var private bool negative;
// Digits array, from least to most significant:
// For example, for 13524:
// `digits[0] = 4`
// `digits[1] = 2`
// `digits[2] = 5`
// `digits[3] = 3`
// `digits[4] = 1`
// Valid `BigInt` should not have this array empty.
var private array<byte> digits;
protected function Constructor()
{
// Init with zero
digits[0] = 0;
}
protected function Finalizer()
{
negative = false;
digits.length = 0;
}
// Minimal `int` value `-2,147,483,648` is slightly a pain to handle, so just
// use this pre-made constructor for it
private static function BigInt CreateMinimalNegative()
{
local array<byte> newDigits;
local BigInt result;
newDigits[0] = 8;
newDigits[1] = 4;
newDigits[2] = 6;
newDigits[3] = 3;
newDigits[4] = 8;
newDigits[5] = 4;
newDigits[6] = 7;
newDigits[7] = 4;
newDigits[8] = 1;
newDigits[9] = 2;
result = BigInt(__().memory.Allocate(class'BigInt'));
result.digits = newDigits;
result.negative = true;
return result;
}
// Removes unnecessary zeroes from leading digit positions `digits`.
// Does not change contained value.
private final static function TrimLeadingZeroes()
{
local int i, zeroesToRemove;
// Find how many leading zeroes there is.
// Since `digits` stores digits from least to most significant, we need
// to check from the end of `digits` array.
for (i = digits.length - 1; i >= 0; i -= 1)
{
if (digits[i] != 0) {
break;
}
zeroesToRemove += 1;
}
// `digits` must not be empty, enforce `0` value in that case
if (zeroesToRemove >= digits.length)
{
digits.length = 1;
digits[0] = 0;
negative = false;
}
else {
digits.length = digits.length - zeroesToRemove;
}
}
public final static function BigInt FromInt(int value)
{
local bool valueIsNegative;
local array<byte> newDigits;
local BigInt result;
local MathAPI.IntegerDivisionResult divisionResult;
if (value < 0)
{
// Treat special case of minimal `int` value `-2,147,483,648` that
// won't fit into positive `int` as special and use pre-made
// specialized constructor `CreateMinimalNegative()`
if (value < -MaxInt) {
return CreateMinimalNegative();
}
else
{
valueIsNegative = true;
value *= -1;
}
}
if (value == 0) {
newDigits[0] = 0;
}
else
{
while (value > 0)
{
divisionResult = __().math.IntegerDivision(value, 10);
value = divisionResult.quotient;
newDigits[newDigits.length] = divisionResult.remainder;
}
}
result = BigInt(__().memory.Allocate(class'BigInt'));
result.digits = newDigits;
result.negative = valueIsNegative;
result.TrimLeadingZeroes();
return result;
}
public final static function BigInt FromDecimal(BaseText value)
{
local int i;
local bool valueIsNegative;
local byte nextDigit;
local array<byte> newDigits;
local Parser parser;
local BigInt result;
local Basetext.Character nextCharacter;
if (value == none) {
return none;
}
parser = value.Parse();
if (parser.Match(P("-")).Ok())
{
valueIsNegative = true;
parser.Confirm();
}
parser.R();
newDigits.length = parser.GetRemainingLength();
i = newDigits.length - 1;
while (!parser.HasFinished())
{
// This should not happen, but just in case
if (i < 0) {
break;
}
parser.MCharacter(nextCharacter);
nextDigit = Clamp(__().text.CharacterToInt(nextCharacter), 0, 9);
newDigits[i] = nextDigit;
i -= 1;
}
result = BigInt(__().memory.Allocate(class'BigInt'));
result.digits = newDigits;
result.negative = valueIsNegative;
parser.FreeSelf();
result.TrimLeadingZeroes();
return result;
}
public final static function BigInt FromDecimal_S(string value)
{
local MutableText wrapper;
local BigInt result;
wrapper = __().text.FromStringM(value);
result = FromDecimal(wrapper);
wrapper.FreeSelf();
return result;
}
private function _add(BigInt other)
{
local int i;
local byte carry, digitSum;
local array<byte> otherDigits;
if (other == none) {
return;
}
otherDigits = other.digits;
if (digits.length < otherDigits.length) {
digits.length = otherDigits.length;
}
carry = 0;
for (i = 0; i < digits.length; i += 1)
{
digitSum = digits[i] + otherDigits[i] + carry;
digits[i] = _.math.Remainder(digitSum, 10);
carry = (digitSum - digits[i]) / 10;
}
if (carry > 0) {
digits[digits.length] = carry;
}
}
public function BigInt Add(BigInt other)
{
_add(other);
return self;
}
public function BigInt AddInt(int other)
{
local BigInt otherObject;
otherObject = FromInt(other);
Add(otherObject);
_.memory.Free(otherObject);
return self;
}
/*public function BigInt Multiply(BigInt other);
public function BigInt MultiplyInt(int other);
public function bool IsNegative();
public function (int other);
public function int ToInt();
public function Text ToText();
public function Text ToText_M();*/
public function Text ToText()
{
return ToText_M().IntoText();
}
public function MutableText ToText_M()
{
local int i;
local MutableText result;
result = _.text.Empty();
if (negative) {
result.AppendCharacter(_.text.GetCharacter("-"));
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result.AppendCharacter(_.text.CharacterFromCodePoint(digits[i] + 48));
}
return result;
}
public function string ToString()
{
local int i;
local string result;
if (negative) {
result = "-";
}
for (i = digits.length - 1; i >= 0; i -= 1) {
result = result $ digits[i];
}
return result;
}
defaultproperties
{
}

106
sources/Data/Database/Tests/TEST_BigInt.uc

@ -0,0 +1,106 @@
/**
* Set of tests for `BigInt` class.
* 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 <https://www.gnu.org/licenses/>.
*/
class TEST_BigInt extends TestCase
abstract;
protected static function TESTS()
{
// Here we use `ToString()` method to check `BigInt` creation,
// therefore also testing it
Test_Creating();
// So here we nee to test `ToText()` methods separately
Test_ToText();
Test_LeadingZeroes();
//Test_AddingValues();
}
// TODO: leading zeroes
protected static function Test_Creating()
{
Context("Testing creation of `BigInt`s.");
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a positive `int`.");
TEST_ExpectTrue(class'BigInt'.static.FromInt(13524).ToString() == "13524");
TEST_ExpectTrue(
class'BigInt'.static.FromInt(MaxInt).ToString() == "2147483647");
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a positive integer inside `string`.");
TEST_ExpectTrue(
class'BigInt'.static.FromDecimal_S("2147483647").ToString()
== "2147483647");
TEST_ExpectTrue(
class'BigInt'.static.FromDecimal_S("4238756872643464981264982128742389")
.ToString() == "4238756872643464981264982128742389");
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a negative `int`.");
TEST_ExpectTrue(class'BigInt'.static.FromInt(-666).ToString() == "-666");
TEST_ExpectTrue(
class'BigInt'.static.FromInt(-MaxInt).ToString() == "-2147483647");
TEST_ExpectTrue(
class'BigInt'.static.FromInt(-MaxInt - 1).ToString() == "-2147483648");
Issue("`ToString()` doesn't return value `BigInt` was initialized with" @
"a negative integer inside `string`.");
TEST_ExpectTrue(
class'BigInt'.static.FromDecimal_S("-2147483648").ToString()
== "-2147483648");
TEST_ExpectTrue(
class'BigInt'.static.FromDecimal_S("-238473846327894632879097410348127")
.ToString() == "-238473846327894632879097410348127");
}
protected static function Test_ToText()
{
Context("Testing `ToText()` method of `BigInt`s.");
Issue("`ToText()` doesn't return value `BigInt` was initialized with" @
"a positive integer inside `string`.");
TEST_ExpectTrue(class'BigInt'.static
.FromDecimal_S("2147483647")
.ToText()
.ToString() == "2147483647");
TEST_ExpectTrue(class'BigInt'.static
.FromDecimal_S("65784236592763459236597823645978236592378659110571388")
.ToText()
.ToString() == "65784236592763459236597823645978236592378659110571388");
Issue("`ToText()` doesn't return value `BigInt` was initialized with" @
"a negative integer inside `string`.");
TEST_ExpectTrue(class'BigInt'.static
.FromDecimal_S("-2147483648")
.ToText()
.ToString() == "-2147483648");
TEST_ExpectTrue(class'BigInt'.static
.FromDecimal_S("-9827657892365923510176386357863078603212901078175829")
.ToText()
.ToString() == "-9827657892365923510176386357863078603212901078175829");
}
/*protected static function Test_AddingValues()
{
Context("Testing adding values to `BigInt`");
Issue("`JSONPointer` is incorrectly extracted.");
}*/
defaultproperties
{
caseGroup = "Database"
caseName = "BigInt"
}

11
sources/Manifest.uc

@ -50,9 +50,10 @@ defaultproperties
testCases(22) = class'TEST_CommandDataBuilder' testCases(22) = class'TEST_CommandDataBuilder'
testCases(23) = class'TEST_LogMessage' testCases(23) = class'TEST_LogMessage'
testCases(24) = class'TEST_SchedulerAPI' testCases(24) = class'TEST_SchedulerAPI'
testCases(25) = class'TEST_DatabaseCommon' testCases(25) = class'TEST_BigInt'
testCases(26) = class'TEST_LocalDatabase' testCases(26) = class'TEST_DatabaseCommon'
testCases(27) = class'TEST_AcediaConfig' testCases(27) = class'TEST_LocalDatabase'
testCases(28) = class'TEST_UTF8EncoderDecoder' testCases(28) = class'TEST_AcediaConfig'
testCases(29) = class'TEST_AvariceStreamReader' testCases(29) = class'TEST_UTF8EncoderDecoder'
testCases(30) = class'TEST_AvariceStreamReader'
} }
Loading…
Cancel
Save