implemented ISignedNumber
.NET Test / .NET tests (push) Successful in 4m27s

This commit is contained in:
2026-09-13 23:08:27 +04:00
parent 082fd84c87
commit e51874b0c3
13 changed files with 1286 additions and 43 deletions
@@ -0,0 +1,115 @@
using System.Globalization;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleNumberStylesTests
{
[Theory]
[InlineData("123", NumberStyles.None, 123.0)]
[InlineData(" -1.25e+2 ", NumberStyles.Float, -125.0)]
[InlineData("1,234.5-", NumberStyles.Number, -1234.5)]
[InlineData("(¤1,234.5)", NumberStyles.Currency, -1234.5)]
[InlineData("1,234.5¤", NumberStyles.Currency, 1234.5)]
[InlineData("(1.25E2)", NumberStyles.Any, -125.0)]
public void DecimalStylesSupportTheirStandardSyntax(string text, NumberStyles style, double expected)
{
DoubleDouble.Parse(text, style, CultureInfo.InvariantCulture).ShouldBe(new DoubleDouble(expected));
DoubleDouble.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture).ShouldBe(new DoubleDouble(expected));
}
[Theory]
[InlineData(" 1", NumberStyles.None)]
[InlineData("1 ", NumberStyles.None)]
[InlineData("-1", NumberStyles.None)]
[InlineData("1-", NumberStyles.Float)]
[InlineData("1.0", NumberStyles.Integer)]
[InlineData("1e2", NumberStyles.Number)]
[InlineData("1,000", NumberStyles.Float)]
[InlineData("(1)", NumberStyles.Number)]
[InlineData("¤1", NumberStyles.Number)]
[InlineData("--1", NumberStyles.Any)]
[InlineData("1.2,3", NumberStyles.Any)]
[InlineData("(1)-", NumberStyles.Any)]
[InlineData("1e+", NumberStyles.Any)]
public void DisallowedOrMalformedSyntaxFailsWithZero(string text, NumberStyles style)
{
DoubleDouble.TryParse(text, style, CultureInfo.InvariantCulture, out DoubleDouble result).ShouldBeFalse();
result.ShouldBe(DoubleDouble.Zero);
DoubleDouble.TryParse(text.AsSpan(), style, CultureInfo.InvariantCulture, out result).ShouldBeFalse();
result.ShouldBe(DoubleDouble.Zero);
Should.Throw<FormatException>(() => DoubleDouble.Parse(text, style, CultureInfo.InvariantCulture));
Should.Throw<FormatException>(() => DoubleDouble.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture));
}
[Theory]
[InlineData(NumberStyles.HexNumber)]
[InlineData(NumberStyles.BinaryNumber)]
[InlineData((NumberStyles)1024)]
[InlineData((NumberStyles)(-1))]
public void InvalidStylesThrowEvenForNullTryParse(NumberStyles style)
{
Should.Throw<ArgumentException>(() => DoubleDouble.Parse("1", style, null));
Should.Throw<ArgumentException>(() => DoubleDouble.Parse("1".AsSpan(), style, null));
Should.Throw<ArgumentException>(() => DoubleDouble.TryParse((string?)null, style, null, out _));
Should.Throw<ArgumentException>(() => DoubleDouble.TryParse(ReadOnlySpan<char>.Empty, style, null, out _));
}
[Fact]
public void NullAndLengthContractsArePreserved()
{
Should.Throw<ArgumentNullException>(() => DoubleDouble.Parse(null!, NumberStyles.Any, null));
DoubleDouble.TryParse((string?)null, NumberStyles.Any, null, out DoubleDouble result).ShouldBeFalse();
result.ShouldBe(DoubleDouble.Zero);
DoubleDouble.TryParse(new string('0', 2049), NumberStyles.Any, null, out result).ShouldBeFalse();
DoubleDouble.Parse(new string('0', 2048), NumberStyles.None, null).ShouldBe(DoubleDouble.Zero);
DoubleDouble.TryParse("1,000", CultureInfo.InvariantCulture, out _).ShouldBeFalse();
}
[Fact]
public void CultureAndExponentPreserveExactComponents()
{
NumberFormatInfo info = new()
{
NegativeSign = "minus",
PositiveSign = "plus",
NumberDecimalSeparator = ";;",
NumberGroupSeparator = "_",
CurrencyDecimalSeparator = ":",
CurrencyGroupSeparator = "~",
CurrencySymbol = "USD"
};
DoubleDouble.Parse("minus9_007_199_254_740_993;;0Eplus0", NumberStyles.Any, info)
.ShouldBe(DoubleDouble.FromComponents(-9007199254740992.0, -1.0));
DoubleDouble.Parse("USD9~007~199~254~740~993:0", NumberStyles.Currency, info)
.ShouldBe(DoubleDouble.FromComponents(9007199254740992.0, 1.0));
}
[Theory]
[InlineData("-0", -0.0)]
[InlineData("-1e-9999", -0.0)]
[InlineData("1e9999", double.PositiveInfinity)]
[InlineData("-Infinity", double.NegativeInfinity)]
[InlineData("NaN", double.NaN)]
public void NonfiniteAndSignedZeroContractsArePreserved(string text, double expected)
{
DoubleDouble value = DoubleDouble.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture);
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
value.Low.ShouldBe(0.0);
}
[Fact]
public void StyledParsingRetainsExactIntegerResidualAcrossOverloads()
{
const string text = " 9,007,199,254,740,993 ";
NumberStyles style = NumberStyles.Number;
DoubleDouble expected = DoubleDouble.FromComponents(9007199254740992.0, 1.0);
DoubleDouble.Parse(text, style, CultureInfo.InvariantCulture).ShouldBe(expected);
DoubleDouble.Parse(text.AsSpan(), style, CultureInfo.InvariantCulture).ShouldBe(expected);
DoubleDouble.TryParse(text, style, CultureInfo.InvariantCulture, out DoubleDouble fromString).ShouldBeTrue();
fromString.ShouldBe(expected);
DoubleDouble.TryParse(text.AsSpan(), style, CultureInfo.InvariantCulture, out DoubleDouble fromSpan).ShouldBeTrue();
fromSpan.ShouldBe(expected);
}
}
@@ -170,17 +170,17 @@ public class DoubleDoubleParsingTests
[Fact]
public void InputLengthIsBoundedBeforeIgnoringWhitespaceOrLeadingZeros()
{
AssertParsed(new string('0', 4095) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
AssertParsed("1" + new string('0', 4095), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 4096) + "1");
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 4096) + "1");
AssertParsed(new string('0', 2047) + "1", CultureInfo.InvariantCulture, 1.0, 0.0);
AssertParsed("1" + new string('0', 2047), CultureInfo.InvariantCulture, double.PositiveInfinity, 0.0);
InvalidInputFailsWithoutLeavingAPartialResult(new string('0', 2048) + "1");
InvalidInputFailsWithoutLeavingAPartialResult(new string(' ', 2048) + "1");
}
[Fact]
public void LongMantissasCanCancelLargeExponentsWithoutOverflowOrUnderflow()
{
AssertParsed("1" + new string('0', 4000) + "e-4000", CultureInfo.InvariantCulture, 1.0, 0.0);
AssertParsed("0." + new string('0', 4000) + "1e4001", CultureInfo.InvariantCulture, 1.0, 0.0);
AssertParsed("1" + new string('0', 2000) + "e-2000", CultureInfo.InvariantCulture, 1.0, 0.0);
AssertParsed("0." + new string('0', 2000) + "1e2001", CultureInfo.InvariantCulture, 1.0, 0.0);
}
[Fact]
@@ -0,0 +1,168 @@
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleSignedNumberTests
{
[Fact]
public void GenericSignedNumberExposesNegativeOneAndBinaryRadix()
{
DoubleDouble value = NegativeOne<DoubleDouble>();
value.High.ShouldBe(-1.0);
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
DoubleDouble.Radix.ShouldBe(2);
}
[Theory]
[InlineData(0.0)]
[InlineData(1.0)]
[InlineData(-1.5)]
[InlineData(double.Epsilon)]
[InlineData(-double.Epsilon)]
[InlineData(double.MaxValue)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]
[InlineData(double.NaN)]
public void ScalarClassificationMatchesBinary64(double scalar)
{
DoubleDouble value = new(scalar);
DoubleDouble.IsCanonical(value).ShouldBeTrue();
DoubleDouble.IsComplexNumber(value).ShouldBeFalse();
DoubleDouble.IsImaginaryNumber(value).ShouldBeFalse();
DoubleDouble.IsRealNumber(value).ShouldBe(!double.IsNaN(scalar));
DoubleDouble.IsPositive(value).ShouldBe(double.IsPositive(value.High));
DoubleDouble.IsNormal(value).ShouldBe(double.IsNormal(scalar));
DoubleDouble.IsSubnormal(value).ShouldBe(double.IsSubnormal(scalar));
DoubleDouble.IsZero(value).ShouldBe(scalar == 0.0);
DoubleDouble.IsInteger(value).ShouldBe(double.IsInteger(scalar));
DoubleDouble.IsEvenInteger(value).ShouldBe(double.IsEvenInteger(scalar));
DoubleDouble.IsOddInteger(value).ShouldBe(double.IsOddInteger(scalar));
}
[Theory]
[InlineData(9007199254740992.0, 1.0, true, false)]
[InlineData(9007199254740992.0, -1.0, true, false)]
[InlineData(18014398509481984.0, 2.0, true, true)]
[InlineData(1.0, 5.551115123125783e-17, false, false)]
[InlineData(9007199254740992.0, 0.5, false, false)]
public void IntegerClassificationIncludesResidual(double high, double low, bool isIntegral, bool even)
{
// Exact binary sums: the residual determines fractional bits and parity above 2^53.
foreach (DoubleDouble value in new[] { DoubleDouble.FromComponents(high, low), -DoubleDouble.FromComponents(high, low) })
{
DoubleDouble.IsInteger(value).ShouldBe(isIntegral);
DoubleDouble.IsEvenInteger(value).ShouldBe(even);
DoubleDouble.IsOddInteger(value).ShouldBe(isIntegral && !even);
DoubleDouble.IsCanonical(value).ShouldBeTrue();
}
}
[Fact]
public void CanonicalClassificationRejectsUnnormalizedAndNoncanonicalRawPairs()
{
DoubleDouble[] values = [new(1.0, 1.0), new(0.0, 1.0), new(1.0, -0.0),
new(double.PositiveInfinity, 1.0), new(double.NaN, 1.0),
new(BitConverter.Int64BitsToDouble(0x7ff8000000000001L), 0.0)];
foreach (DoubleDouble value in values)
{
DoubleDouble.IsCanonical(value).ShouldBeFalse();
}
}
[Fact]
public void NegativeZeroClassificationPreservesSign()
{
DoubleDouble value = new(-0.0);
DoubleDouble.IsCanonical(value).ShouldBeTrue();
DoubleDouble.IsZero(value).ShouldBeTrue();
DoubleDouble.IsPositive(value).ShouldBeFalse();
DoubleDouble.IsNegative(value).ShouldBeTrue();
DoubleDouble.IsEvenInteger(value).ShouldBeTrue();
DoubleDouble.IsOddInteger(value).ShouldBeFalse();
}
[Fact]
public void MagnitudeSelectionUsesBothComponentsAndBreaksTiesBySign()
{
DoubleDouble smaller = new(1.0);
DoubleDouble larger = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54));
foreach ((DoubleDouble left, DoubleDouble right) in new[] { (smaller, -larger), (-larger, smaller) })
{
DoubleDouble.MaxMagnitude(left, right).ShouldBe(-larger);
DoubleDouble.MaxMagnitudeNumber(left, right).ShouldBe(-larger);
DoubleDouble.MinMagnitude(left, right).ShouldBe(smaller);
DoubleDouble.MinMagnitudeNumber(left, right).ShouldBe(smaller);
}
foreach ((DoubleDouble left, DoubleDouble right) in new[] { (larger, -larger), (-larger, larger) })
{
DoubleDouble.MaxMagnitude(left, right).ShouldBe(larger);
DoubleDouble.MinMagnitude(left, right).ShouldBe(-larger);
}
}
[Fact]
public void MagnitudeSpecialValuesMatchBinary64()
{
double[] values = [0.0, -0.0, 1.0, -1.0, double.Epsilon, double.MaxValue,
double.PositiveInfinity, double.NegativeInfinity, double.NaN];
foreach (double left in values)
{
foreach (double right in values)
{
AssertBits(DoubleDouble.MaxMagnitude(new(left), new(right)), double.MaxMagnitude(left, right));
AssertBits(DoubleDouble.MinMagnitude(new(left), new(right)), double.MinMagnitude(left, right));
AssertBits(DoubleDouble.MaxMagnitudeNumber(new(left), new(right)), double.MaxMagnitudeNumber(left, right));
AssertBits(DoubleDouble.MinMagnitudeNumber(new(left), new(right)), double.MinMagnitudeNumber(left, right));
}
AssertBits(DoubleDouble.Abs(new(left)), double.IsNaN(left) ? double.NaN : double.Abs(left));
}
DoubleDouble negative = DoubleDouble.FromComponents(-1.0, Math.ScaleB(1.0, -54));
DoubleDouble.Abs(negative).High.ShouldBe(1.0);
DoubleDouble.Abs(negative).Low.ShouldBe(-Math.ScaleB(1.0, -54));
}
[Fact]
public void IncrementAndDecrementRetainResidualAndSupportGenericDispatch()
{
// 2^53 +/- 1 are exact two-component integers.
DoubleDouble start = new(9007199254740992.0);
DoubleDouble incremented = Increment(start);
incremented.High.ShouldBe(9007199254740992.0);
incremented.Low.ShouldBe(1.0);
DoubleDouble decremented = Decrement(incremented);
decremented.ShouldBe(start);
DoubleDouble post = start++;
post.High.ShouldBe(9007199254740992.0);
start.Low.ShouldBe(1.0);
post = start--;
post.Low.ShouldBe(1.0);
start.Low.ShouldBe(0.0);
Increment(DoubleDouble.NegativeOne).ShouldBe(DoubleDouble.Zero);
Decrement(DoubleDouble.One).ShouldBe(DoubleDouble.Zero);
Increment(DoubleDouble.NaN).ShouldBe(DoubleDouble.NaN);
AssertBits(Decrement(new DoubleDouble(double.NegativeInfinity)), double.NegativeInfinity);
}
private static T Increment<T>(T value) where T : ISignedNumber<T>
{
return ++value;
}
private static T Decrement<T>(T value) where T : ISignedNumber<T>
{
return --value;
}
private static void AssertBits(DoubleDouble actual, double expected)
{
BitConverter.DoubleToInt64Bits(actual.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
BitConverter.DoubleToInt64Bits(actual.Low).ShouldBe(0L);
}
private static T NegativeOne<T>() where T : ISignedNumber<T>
{
return T.NegativeOne;
}
}
@@ -0,0 +1,70 @@
using System.Globalization;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleSpanFormattingTests
{
[Theory]
[InlineData("")]
[InlineData("G32")]
[InlineData("g999")]
[InlineData("E20")]
[InlineData("F54")]
public void SpanFormattingMatchesExactComponentFormatting(string format)
{
DoubleDouble value = DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54));
NumberFormatInfo provider = new() { NumberDecimalSeparator = "::", NegativeSign = "minus" };
string expected = value.ToString(format, provider);
char[] buffer = new char[expected.Length + 1];
buffer[^1] = '!';
((ISpanFormattable)value).TryFormat(buffer.AsSpan(0, expected.Length), out int written, format, provider).ShouldBeTrue();
written.ShouldBe(expected.Length);
new string(buffer, 0, written).ShouldBe(expected);
buffer[^1].ShouldBe('!');
}
[Fact]
public void DefaultSpanFormattingRetainsLowComponentDigits()
{
Span<char> buffer = stackalloc char[64];
DoubleDouble.FromComponents(1.0, Math.ScaleB(1.0, -54))
.TryFormat(buffer, out int written, provider: CultureInfo.InvariantCulture).ShouldBeTrue();
buffer[..written].ToString().ShouldBe("1.0000000000000000555111512312578");
}
[Fact]
public void ShortDestinationIsUnchangedAndReportsZero()
{
char[] buffer = ['!', '!'];
DoubleDouble.PI.TryFormat(buffer, out int written, "G32", CultureInfo.InvariantCulture).ShouldBeFalse();
written.ShouldBe(0);
new string(buffer).ShouldBe("!!");
DoubleDouble.One.TryFormat(Span<char>.Empty, out written, provider: CultureInfo.InvariantCulture).ShouldBeFalse();
written.ShouldBe(0);
}
[Theory]
[InlineData(-0.0)]
[InlineData(double.NaN)]
[InlineData(double.PositiveInfinity)]
[InlineData(double.NegativeInfinity)]
public void SpanFormattingPreservesSpecialValues(double high)
{
NumberFormatInfo provider = new() { NegativeSign = "minus", NaNSymbol = "unknown" };
DoubleDouble value = new(high);
Span<char> buffer = stackalloc char[64];
value.TryFormat(buffer, out int written, "F2", provider).ShouldBeTrue();
buffer[..written].ToString().ShouldBe(value.ToString("F2", provider));
}
[Theory]
[InlineData("R")]
[InlineData("F1000")]
[InlineData("G-1")]
public void InvalidFormatThrowsEvenForEmptyDestination(string format)
{
Should.Throw<FormatException>(() => DoubleDouble.One.TryFormat(Span<char>.Empty, out _, format, CultureInfo.InvariantCulture));
}
}
@@ -0,0 +1,108 @@
using System.Globalization;
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public class DoubleDoubleSpecialValueTests
{
[Theory]
[InlineData("+NaN")]
[InlineData("-NaN")]
[InlineData("+Infinity")]
[InlineData("-Infinity")]
[InlineData(" NaN ")]
[InlineData(" \t+Infinity\r\n")]
public void StandardSpecialValuesMatchBinary64WithoutStyleFlags(string text)
{
double.TryParse(text, NumberStyles.None, CultureInfo.InvariantCulture, out double expected).ShouldBeTrue();
AssertAllParsers(text, CultureInfo.InvariantCulture, expected);
}
[Theory]
[InlineData("inf", double.PositiveInfinity)]
[InlineData("+INF", double.PositiveInfinity)]
[InlineData("-iNf", double.NegativeInfinity)]
[InlineData(" \t-inf\r\n", double.NegativeInfinity)]
public void ShortInfinityAliasIsCaseInsensitive(string text, double expected)
{
AssertAllParsers(text, CultureInfo.InvariantCulture, expected);
}
[Fact]
public void ShortInfinityUsesCultureSignsAndRespectsCustomSymbolPrecedence()
{
NumberFormatInfo info = new() { PositiveSign = "plus", NegativeSign = "minus" };
AssertAllParsers(" minusINF ", info, double.NegativeInfinity);
AssertAllParsers("plusinf", info, double.PositiveInfinity);
info.NaNSymbol = "inf";
AssertAllParsers("inf", info, double.NaN);
info.NaNSymbol = "missing";
info.NegativeInfinitySymbol = "inf";
AssertAllParsers("inf", info, double.NegativeInfinity);
}
[Theory]
[InlineData("in")]
[InlineData("infx")]
[InlineData("infinite")]
[InlineData("--inf")]
[InlineData("+ inf")]
[InlineData("inf-")]
public void MalformedAliasesAreRejected(string text)
{
DoubleDouble.TryParse(text, CultureInfo.InvariantCulture, out DoubleDouble result).ShouldBeFalse();
result.ShouldBe(DoubleDouble.Zero);
DoubleDouble.TryParse(text.AsSpan(), NumberStyles.Any, CultureInfo.InvariantCulture, out result).ShouldBeFalse();
result.ShouldBe(DoubleDouble.Zero);
}
[Fact]
public void SpecialValuesStillRespectRawLengthAndInvalidStyles()
{
AssertAllParsers(new string(' ', 2045) + "inf", CultureInfo.InvariantCulture, double.PositiveInfinity);
string tooLong = new string(' ', 2046) + "inf";
DoubleDouble.TryParse(tooLong, CultureInfo.InvariantCulture, out _).ShouldBeFalse();
DoubleDouble.TryParse(tooLong.AsSpan(), NumberStyles.None, CultureInfo.InvariantCulture, out _).ShouldBeFalse();
Should.Throw<FormatException>(() => DoubleDouble.Parse(tooLong, NumberStyles.None, CultureInfo.InvariantCulture));
Should.Throw<ArgumentException>(() => DoubleDouble.TryParse("inf", NumberStyles.HexNumber, null, out _));
DoubleDouble.TryParse(" +1 ", NumberStyles.None, CultureInfo.InvariantCulture, out _).ShouldBeFalse();
}
[Fact]
public void AbsCanonicalizesRawNaNPayloadsOfEitherSign()
{
foreach (long bits in new[] { 0x7ff8000000000001L, unchecked((long)0xfff8000000000001UL) })
{
DoubleDouble result = DoubleDouble.Abs(new DoubleDouble(BitConverter.Int64BitsToDouble(bits), 0.0));
BitConverter.DoubleToInt64Bits(result.High).ShouldBe(BitConverter.DoubleToInt64Bits(double.NaN));
BitConverter.DoubleToInt64Bits(result.Low).ShouldBe(0L);
DoubleDouble.IsCanonical(result).ShouldBeTrue();
}
}
private static void AssertAllParsers(string text, IFormatProvider provider, double expected)
{
DoubleDouble.TryParse(text, provider, out DoubleDouble fromString).ShouldBeTrue();
DoubleDouble.TryParse(text.AsSpan(), provider, out DoubleDouble fromSpan).ShouldBeTrue();
DoubleDouble[] values = [fromString, fromSpan, DoubleDouble.Parse(text, provider), DoubleDouble.Parse(text.AsSpan(), provider),
ParseStyled<DoubleDouble>(text, provider)];
foreach (DoubleDouble value in values)
{
BitConverter.DoubleToInt64Bits(value.High).ShouldBe(BitConverter.DoubleToInt64Bits(expected));
BitConverter.DoubleToInt64Bits(value.Low).ShouldBe(0L);
}
}
private static T ParseStyled<T>(string text, IFormatProvider provider) where T : INumberBase<T>
{
T.TryParse(text, NumberStyles.None, provider, out T? fromString).ShouldBeTrue();
T.TryParse(text.AsSpan(), NumberStyles.None, provider, out T? fromSpan).ShouldBeTrue();
T result = T.Parse(text, NumberStyles.None, provider);
fromString.ShouldBe(result);
fromSpan.ShouldBe(result);
T.Parse(text.AsSpan(), NumberStyles.None, provider).ShouldBe(result);
return result;
}
}
@@ -0,0 +1,151 @@
using System.Diagnostics.CodeAnalysis;
using System.Numerics;
using Shouldly;
using Xunit;
namespace Just.PreciseMath.Tests;
public sealed class GenericConversionTests
{
[Fact]
public void GenericCreationPreservesComponentsAndExactIntegers()
{
DoubleDouble value = DoubleDouble.FromComponents(1.0, double.Epsilon);
DoubleDouble.CreateChecked(value).Low.ShouldBe(double.Epsilon);
DoubleDouble.CreateSaturating(value).Low.ShouldBe(double.Epsilon);
DoubleDouble.CreateTruncating(value).Low.ShouldBe(double.Epsilon);
DoubleDouble.CreateChecked(ulong.MaxValue).High.ShouldBe(Math.ScaleB(1.0, 64));
DoubleDouble.CreateChecked(ulong.MaxValue).Low.ShouldBe(-1.0);
DoubleDouble.CreateChecked(decimal.MaxValue).Low.ShouldBe(-1.0);
BigInteger integer = (BigInteger.One << 100) + 1;
BigInteger.CreateChecked(DoubleDouble.CreateChecked(integer)).ShouldBe(integer);
BitConverter.DoubleToInt64Bits(DoubleDouble.CreateChecked(-0.0).High).ShouldBe(long.MinValue);
}
[Fact]
public void IntegerTargetsTruncateTheExactSumBeforeApplyingRangePolicy()
{
int.CreateChecked(DoubleDouble.FromComponents(1.0, -double.Epsilon)).ShouldBe(0);
int.CreateChecked(DoubleDouble.FromComponents(-1.0, double.Epsilon)).ShouldBe(0);
long.CreateChecked(DoubleDouble.FromComponents(Math.ScaleB(1.0, 63), -1.0)).ShouldBe(long.MaxValue);
ulong.CreateChecked(DoubleDouble.FromComponents(Math.ScaleB(1.0, 64), -1.0)).ShouldBe(ulong.MaxValue);
DoubleDouble overflow = new(256.75);
Should.Throw<OverflowException>(() => byte.CreateChecked(overflow));
byte.CreateSaturating(overflow).ShouldBe(byte.MaxValue);
byte.CreateTruncating(overflow).ShouldBe((byte)0);
byte.CreateSaturating(new DoubleDouble(-1.0)).ShouldBe((byte)0);
byte.CreateTruncating(new DoubleDouble(-1.0)).ShouldBe(byte.MaxValue);
}
[Fact]
public void HugeIntegerInputsUseFloatingOverflowAndExactRatioRounding()
{
BigInteger huge = BigInteger.One << 2000;
double.IsPositiveInfinity(DoubleDouble.CreateChecked(huge).High).ShouldBeTrue();
double.IsPositiveInfinity(DoubleDouble.CreateSaturating(huge).High).ShouldBeTrue();
double.IsNegativeInfinity(DoubleDouble.CreateTruncating(-huge).High).ShouldBeTrue();
// A sparse exact integer must retain its low component rather than pass through double.
DoubleDouble sparse = DoubleDouble.CreateChecked((BigInteger.One << 100) + 1);
sparse.High.ShouldBe(Math.ScaleB(1.0, 100));
sparse.Low.ShouldBe(1.0);
}
[Fact]
public void FloatingAndDecimalTargetsRetainResidualRounding()
{
DoubleDouble aboveFloatMidpoint = DoubleDouble.FromComponents(1.0 + Math.ScaleB(1.0, -24), double.Epsilon);
float.CreateChecked(aboveFloatMidpoint).ShouldBe(float.BitIncrement(1.0f));
DoubleDouble aboveHalfMidpoint = DoubleDouble.FromComponents(1.0 + Math.ScaleB(1.0, -11), double.Epsilon);
Half.CreateChecked(aboveHalfMidpoint).ShouldBe(Half.BitIncrement((Half)1));
decimal.CreateChecked(DoubleDouble.CreateChecked(0.1m)).ShouldBe(0.1m);
Should.Throw<OverflowException>(() => decimal.CreateChecked(new DoubleDouble(double.PositiveInfinity)));
decimal.CreateSaturating(new DoubleDouble(double.PositiveInfinity)).ShouldBe(decimal.MaxValue);
decimal.CreateTruncating(DoubleDouble.NaN).ShouldBe(0m);
int.CreateSaturating(DoubleDouble.NaN).ShouldBe(0);
int.CreateTruncating(new DoubleDouble(double.PositiveInfinity)).ShouldBe(int.MaxValue);
Should.Throw<OverflowException>(() => int.CreateChecked(DoubleDouble.NaN));
}
[Fact]
public void BuiltInNumericFamiliesRoundTripThroughAllCreationModes()
{
RoundTrip((byte)123);
RoundTrip((sbyte)-123);
RoundTrip((short)-12345);
RoundTrip((ushort)54321);
RoundTrip(int.MinValue);
RoundTrip(uint.MaxValue);
RoundTrip(long.MinValue);
RoundTrip(ulong.MaxValue);
RoundTrip((nint)(-12345));
RoundTrip((nuint)54321);
RoundTrip((Int128.One << 100) + 1);
RoundTrip((UInt128.One << 100) + 1);
RoundTrip('A');
RoundTrip((Half)1.25);
RoundTrip(1.25f);
RoundTrip(1.25);
RoundTrip(1.25m);
}
private static void RoundTrip<T>(T value) where T : INumberBase<T>
{
T.CreateChecked(DoubleDouble.CreateChecked(value)).ShouldBe(value);
T.CreateSaturating(DoubleDouble.CreateSaturating(value)).ShouldBe(value);
T.CreateTruncating(DoubleDouble.CreateTruncating(value)).ShouldBe(value);
}
[Fact]
public void UnsupportedHooksReturnFalseWithoutTwoSidedRecursion()
{
ConversionProbe<DoubleDouble>.FromChecked(Complex.One, out DoubleDouble from).ShouldBeFalse();
from.ShouldBe(DoubleDouble.Zero);
ConversionProbe<DoubleDouble>.FromSaturating(Complex.One, out _).ShouldBeFalse();
ConversionProbe<DoubleDouble>.FromTruncating(Complex.One, out _).ShouldBeFalse();
ConversionProbe<DoubleDouble>.ToChecked(DoubleDouble.One, out Complex to).ShouldBeFalse();
to.ShouldBe(default);
ConversionProbe<DoubleDouble>.ToSaturating(DoubleDouble.One, out Complex _).ShouldBeFalse();
ConversionProbe<DoubleDouble>.ToTruncating(DoubleDouble.One, out Complex _).ShouldBeFalse();
}
private interface ConversionProbe<T> : INumberBase<T> where T : INumberBase<T>
{
public static bool FromChecked<TOther>(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase<TOther>
{
return T.TryConvertFromChecked(value, out result);
}
public static bool FromSaturating<TOther>(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase<TOther>
{
return T.TryConvertFromSaturating(value, out result);
}
public static bool FromTruncating<TOther>(TOther value, [MaybeNullWhen(false)] out T result) where TOther : INumberBase<TOther>
{
return T.TryConvertFromTruncating(value, out result);
}
public static bool ToChecked<TOther>(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
{
return T.TryConvertToChecked(value, out result);
}
public static bool ToSaturating<TOther>(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
{
return T.TryConvertToSaturating(value, out result);
}
public static bool ToTruncating<TOther>(T value, [MaybeNullWhen(false)] out TOther result) where TOther : INumberBase<TOther>
{
return T.TryConvertToTruncating(value, out result);
}
}
[Fact]
public void UnsupportedComplexConversionTerminatesWithNotSupported()
{
Should.Throw<NotSupportedException>(() => DoubleDouble.CreateChecked(Complex.One));
Should.Throw<NotSupportedException>(() => DoubleDouble.CreateSaturating(Complex.One));
Should.Throw<NotSupportedException>(() => DoubleDouble.CreateTruncating(Complex.One));
}
}