Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
7f9864208This file is pinned to the selected release. Download the original to open it in your design tool.
using Lombiq.HelpfulLibraries.Common.Utilities;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Numerics;
using Xunit;
using Xunit.Abstractions;
namespace Hast.Algorithms.Tests;
public class Fix64Tests
{
private readonly ITestOutputHelper _testOutputHelper;
private readonly long[] _testCases =
[
// Small numbers
0L, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
-1, -2, -3, -4, -5, -6, -7, -8, -9, -10,
// Integer numbers
0x_0001_0000_0000, -0x_0001_0000_0000, 0x_0002_0000_0000, -0x_0002_0000_0000, 0x_0003_0000_0000, -0x_0003_0000_0000,
0x_0004_0000_0000, -0x_0004_0000_0000, 0x_0005_0000_0000, -0x_0005_0000_0000, 0x_0006_0000_0000, -0x_0006_0000_0000,
// Fractions (1/2, 1/4, 1/8)
0x_8000_0000, -0x_8000_0000, 0x_4000_0000, -0x_4000_0000, 0x_2000_0000, -0x_2000_0000,
// Problematic carry
0x_FFFF_FFFF, -0x_FFFF_FFFF, 0x_0001_FFFF_FFFF, -0x_0001_FFFF_FFFF, 0x_0003_FFFF_FFFF, -0x_0003_FFFF_FFFF,
// Smallest and largest values
long.MaxValue, long.MinValue,
// Large random numbers
6_791_302_811_978_701_836, -8_192_141_831_180_282_065, 6_222_617_001_063_736_300, -7_871_200_276_881_732_034,
8_249_382_838_880_205_112, -7_679_310_892_959_748_444, 7_708_113_189_940_799_513, -5_281_862_979_887_936_768,
8_220_231_180_772_321_456, -5_204_203_381_295_869_580, 6_860_614_387_764_479_339, -9_080_626_825_133_349_457,
6_658_610_233_456_189_347, -6_558_014_273_345_705_245, 6_700_571_222_183_426_493,
// Small random numbers
-436_730_658, -2_259_913_246, 329_347_474, 2_565_801_981, 3_398_143_698, 137_497_017, 1_060_347_500,
-3_457_686_027, 1_923_669_753, 2_891_618_613, 2_418_874_813, 2_899_594_950, 2_265_950_765, -1_962_365_447,
3_077_934_393
// Tiny random numbers
- 171,
-359, 491, 844, 158, -413, -422, -737, -575, -330,
-376, 435, -311, 116, 715, -1_024, -487, 59, 724, 993,
];
public Fix64Tests(ITestOutputHelper testOutputHelper) => _testOutputHelper = testOutputHelper;
[Fact]
public void Precision() => Assert.Equal(0.00000000023283064365386962890625m, Fix64.Precision);
[Fact]
public void LongToFix64AndBack()
{
var sources = new[] { long.MinValue, int.MinValue - 1L, int.MinValue, -1L, 0L, 1L, int.MaxValue, int.MaxValue + 1L, long.MaxValue };
var expecteds = new[] { 0L, int.MaxValue, int.MinValue, -1L, 0L, 1L, int.MaxValue, int.MinValue, -1L };
for (int i = 0; i < sources.Length; ++i)
{
var expected = expecteds[i];
var f = (Fix64)sources[i];
var actual = (long)f;
Assert.Equal(expected, actual);
}
}
[Fact]
public void DoubleToFix64AndBack()
{
var sources = new[]
{
int.MinValue,
-Math.PI,
-Math.E,
-1.0,
-0.0,
0.0,
1.0,
Math.PI,
Math.E,
int.MaxValue,
};
foreach (var value in sources)
{
EqualWithinPrecision(value, (double)(Fix64)value);
}
}
[Fact]
public void DecimalToFix64AndBack()
{
Assert.Equal(Fix64.MaxValue(), (Fix64)(decimal)Fix64.MaxValue());
Assert.Equal(Fix64.MinValue(), (Fix64)(decimal)Fix64.MinValue());
var sources = new[]
{
int.MinValue,
-(decimal)Math.PI,
-(decimal)Math.E,
-1.0m,
-0.0m,
0.0m,
1.0m,
(decimal)Math.PI,
(decimal)Math.E,
int.MaxValue,
};
foreach (var value in sources)
{
EqualWithinPrecision(value, (decimal)(Fix64)value);
}
}
[Fact]
public void Addition()
{
var terms1 = new[] { Fix64.MinValue(), (Fix64)(-1), Fix64.Zero(), Fix64.One(), Fix64.MaxValue() };
var terms2 = new[] { (Fix64)(-1), (Fix64)2, (Fix64)(-1.5m), (Fix64)(-2), Fix64.One() };
var expecteds = new[] { Fix64.MinValue(), Fix64.One(), (Fix64)(-1.5m), (Fix64)(-1), Fix64.MaxValue() };
for (int i = 0; i < terms1.Length; ++i)
{
var actual = terms1[i] + terms2[i];
var expected = expecteds[i];
Assert.Equal(expected, actual);
}
}
[Fact]
public void Subtraction()
{
var terms1 = new[] { Fix64.MinValue(), (Fix64)(-1), Fix64.Zero(), Fix64.One(), Fix64.MaxValue() };
var terms2 = new[] { Fix64.One(), (Fix64)(-2), (Fix64)1.5m, (Fix64)2, (Fix64)(-1) };
var expecteds = new[] { Fix64.MinValue(), Fix64.One(), (Fix64)(-1.5m), (Fix64)(-1), Fix64.MaxValue() };
for (int i = 0; i < terms1.Length; ++i)
{
var actual = terms1[i] - terms2[i];
var expected = expecteds[i];
Assert.Equal(expected, actual);
}
}
[Fact]
public void BasicMultiplication()
{
var leftTerms = new[] { 0m, 1m, -1m, 5m, -5m, 0.5m, -0.5m, -1.0m };
var rightTerms = new[] { 16m, 16m, 16m, 16m, 16m, 16m, 16m, -1.0m };
var expecteds = new[] { 0L, 16, -16, 80, -80, 8, -8, 1 };
for (int i = 0; i < leftTerms.Length; ++i)
{
var expected = expecteds[i];
var actual = (long)((Fix64)leftTerms[i] * (Fix64)rightTerms[i]);
Assert.Equal(expected, actual);
}
}
[Fact]
public void MultiplicationTestCases()
{
var sw = new Stopwatch();
int failures = 0;
foreach (var (testCaseX, testCaseY) in GetTestCasePairs())
{
var x = Fix64.FromRaw(testCaseX);
var y = Fix64.FromRaw(testCaseY);
var xM = (decimal)x;
var yM = (decimal)y;
var expected = Constrain(xM * yM, (decimal)Fix64.MinValue(), (decimal)Fix64.MaxValue());
sw.Start();
var actual = x * y;
sw.Stop();
var actualM = (decimal)actual;
var maxDelta = (decimal)Fix64.FromRaw(1);
if (Math.Abs(actualM - expected) > maxDelta)
{
_testOutputHelper.WriteLine(
"Failed for FromRaw({0}) * FromRaw({1}): expected {2} but got {3}",
testCaseX,
testCaseY,
(Fix64)expected,
actualM);
++failures;
}
}
_testOutputHelper.WriteLine(
"{0} total, {1} per multiplication",
sw.ElapsedMilliseconds,
(double)sw.Elapsed.Milliseconds / (_testCases.Length * _testCases.Length));
Assert.True(failures < 1);
}
[Fact]
public void DivisionTestCases()
{
var sw = new Stopwatch();
int failures = 0;
foreach (var (testCaseX, testCaseY) in GetTestCasePairs())
{
var x = Fix64.FromRaw(testCaseX);
var y = Fix64.FromRaw(testCaseY);
var xM = (decimal)x;
var yM = (decimal)y;
if (testCaseY == 0)
{
// Hastlayer doesn't handle exceptions.
Assert.True(x / y == default);
}
else
{
var expected = Constrain(xM / yM, (decimal)Fix64.MinValue(), (decimal)Fix64.MaxValue());
sw.Start();
var actual = x / y;
sw.Stop();
var actualM = (decimal)actual;
var maxDelta = (decimal)Fix64.FromRaw(1);
if (Math.Abs(actualM - expected) > maxDelta)
{
_testOutputHelper.WriteLine(
"Failed for FromRaw({0}) / FromRaw({1}): expected {2} but got {3}",
testCaseX,
testCaseY,
(Fix64)expected,
actualM);
++failures;
}
}
}
_testOutputHelper.WriteLine(
"{0} total, {1} per division",
sw.ElapsedMilliseconds,
(double)sw.Elapsed.Milliseconds / (_testCases.Length * _testCases.Length));
Assert.True(failures < 1);
}
[Fact]
public void Sign()
{
var sources = new[] { Fix64.MinValue(), (Fix64)(-1), Fix64.Zero(), Fix64.One(), Fix64.MaxValue() };
var expecteds = new[] { -1, -1, 0, 1, 1 };
for (int i = 0; i < sources.Length; ++i)
{
var actual = Fix64.Sign(sources[i]);
var expected = expecteds[i];
Assert.Equal(expected, actual);
}
}
[Fact]
public void Abs()
{
Assert.Equal(Fix64.MaxValue(), Fix64.Abs(Fix64.MinValue()));
var sources = new[] { -1, 0, 1, int.MaxValue };
var expecteds = new[] { 1, 0, 1, int.MaxValue };
for (int i = 0; i < sources.Length; ++i)
{
var actual = Fix64.Abs((Fix64)sources[i]);
var expected = (Fix64)expecteds[i];
Assert.Equal(expected, actual);
}
}
[Fact]
public void FastAbs()
{
Assert.Equal(Fix64.MinValue(), Fix64.FastAbs(Fix64.MinValue()));
var sources = new[] { -1, 0, 1, int.MaxValue };
var expecteds = new[] { 1, 0, 1, int.MaxValue };
for (int i = 0; i < sources.Length; ++i)
{
var actual = Fix64.FastAbs((Fix64)sources[i]);
var expected = (Fix64)expecteds[i];
Assert.Equal(expected, actual);
}
}
[Fact]
public void Floor()
{
var sources = new[] { -5.1m, -1, 0, 1, 5.1m };
var expecteds = new[] { -6m, -1, 0, 1, 5m };
for (int i = 0; i < sources.Length; ++i)
{
var actual = (decimal)Fix64.Floor((Fix64)sources[i]);
var expected = expecteds[i];
Assert.Equal(expected, actual);
}
}
[Fact]
public void Ceiling()
{
var sources = new[] { -5.1m, -1, 0, 1, 5.1m };
var expecteds = new[] { -5m, -1, 0, 1, 6m };
for (int i = 0; i < sources.Length; ++i)
{
var actual = (decimal)Fix64.Ceiling((Fix64)sources[i]);
var expected = expecteds[i];
Assert.Equal(expected, actual);
}
Assert.Equal(Fix64.MaxValue(), Fix64.Ceiling(Fix64.MaxValue()));
}
[Fact]
public void Round()
{
var sources = new[] { -5.5m, -5.1m, -4.5m, -4.4m, -1, 0, 1, 4.5m, 4.6m, 5.4m, 5.5m };
var expecteds = new[] { -6m, -5m, -4m, -4m, -1, 0, 1, 4m, 5m, 5m, 6m };
for (int i = 0; i < sources.Length; ++i)
{
var actual = (decimal)Fix64.Round((Fix64)sources[i]);
var expected = expecteds[i];
Assert.Equal(expected, actual);
}
Assert.Equal(Fix64.MaxValue(), Fix64.Round(Fix64.MaxValue()));
}
[Fact]
public void Sqrt()
{
foreach (var testCase in _testCases)
{
var f = Fix64.FromRaw(testCase);
if (Fix64.Sign(f) < 0)
{
Assert.Throws<ArgumentOutOfRangeException>(() => Fix64.Sqrt(f));
}
else
{
var expected = Math.Sqrt((double)f);
var actual = (double)Fix64.Sqrt(f);
var delta = (decimal)Math.Abs(expected - actual);
Assert.True(delta <= Fix64.Precision);
}
}
}
[Fact]
public void Modulus()
{
var deltas = new List<decimal>();
foreach (var (operand1, operand2) in GetTestCasePairs())
{
var f1 = Fix64.FromRaw(operand1);
var f2 = Fix64.FromRaw(operand2);
if (operand2 == 0)
{
// Hastlayer doesn't handle exceptions.
Assert.True(f1 / f2 == default);
}
else
{
var actual = (decimal)(f1 % f2);
var expected = GetExpectedModulus(operand1, operand2);
var delta = Math.Abs(expected - actual);
deltas.Add(delta);
Assert.True(
delta <= 60 * Fix64.Precision,
StringHelper.CreateInvariant($"{f1} % {f2} = expected {expected} but got {actual}"));
}
}
_testOutputHelper.WriteLine("Max error: {0} ({1} times precision)", deltas.Max(), deltas.Max() / Fix64.Precision);
_testOutputHelper.WriteLine("Average precision: {0} ({1} times precision)", deltas.Average(), deltas.Average() / Fix64.Precision);
_testOutputHelper.WriteLine("failed: {0}%", deltas.Count(d => d > Fix64.Precision) * 100.0 / deltas.Count);
}
[Fact]
public void Negation()
{
foreach (var operand1 in _testCases)
{
var f = Fix64.FromRaw(operand1);
if (f == Fix64.MinValue())
{
Assert.Equal(-f, Fix64.MaxValue());
}
else
{
var expected = -(decimal)f;
var actual = (decimal)-f;
Assert.Equal(expected, actual);
}
}
}
[Fact]
public void EqualsMethod()
{
foreach (var (op1, op2) in GetTestCasePairs())
{
var d1 = (decimal)op1;
var d2 = (decimal)op2;
Assert.True(op1.Equals(op2) == d1.Equals(d2));
}
}
[Fact]
[SuppressMessage(
"Major Bug",
"S1244:Floating point numbers should not be tested for equality",
Justification = "They should match precisely.")]
[SuppressMessage(
"Major Bug",
"S2589:Change this condition so that it does not always evaluate to 'false'.",
Justification = "We are trying to verify that, so it's a success state rather than a certainty.")]
public void EqualityAndInequalityOperators()
{
var sources = _testCases.Select(Fix64.FromRaw).ToList();
foreach (var op1 in sources)
{
foreach (var op2 in sources)
{
var d1 = (double)op1;
var d2 = (double)op2;
Assert.True(op1 == op2 == (d1 == d2));
Assert.True(op1 != op2 == (d1 != d2));
Assert.False(op1 == op2 && op1 != op2);
}
}
}
// Not needed temporarily because we removed the interface implementations from Fix64.
////[Fact]
////public void CompareTo()
////{
//// var nums = _testCases.Select(Fix64.FromRaw).ToArray();
//// var numsDecimal = nums.Select(t => (decimal)t).ToArray();
//// Array.Sort(nums);
//// Array.Sort(numsDecimal);
//// Assert.True(nums.Select(t => (decimal)t).SequenceEqual(numsDecimal));
////}
[Fact]
public void SerializationToAndFromIntegers()
{
foreach (var testCase in _testCases.Select(Fix64.FromRaw))
{
Assert.Equal(testCase, Fix64.FromRawInts(testCase.ToIntegers()));
}
}
private IEnumerable<(long TestCaseX, long TestCaseY)> GetTestCasePairs() => _testCases.CartesianProduct();
private static void EqualWithinPrecision(decimal value1, decimal value2) =>
Assert.True(Math.Abs(value2 - value1) < Fix64.Precision);
private static void EqualWithinPrecision(double value1, double value2) =>
Assert.True(Math.Abs(value2 - value1) < (double)Fix64.Precision);
private static decimal GetExpectedModulus(long operand1, long operand2)
{
var dividend = new BigInteger(operand1);
var divisor = new BigInteger(operand2);
var remainder = dividend % divisor;
return (decimal)Fix64.FromRaw((long)remainder);
}
[SuppressMessage(
"Major Code Smell",
"S3358:Ternary operators should not be nested",
Justification = "That's why it's encapsulated into a method.")]
private static decimal Constrain(decimal expected, decimal min, decimal max) =>
expected > max
? max
: expected < min
? min
: expected;
}