diff --git a/BREAKING-CHANGES.md b/BREAKING-CHANGES.md index 5875d2c1c..eea4df040 100644 --- a/BREAKING-CHANGES.md +++ b/BREAKING-CHANGES.md @@ -72,6 +72,24 @@ solver's check that a limit agrees from both sides goes through the same compari | `AreEqualNumerically("2 x (-1/4 + ln(x)/2) + 1/x * 2/4 * x^2", "x ln(x)")` | `false` | `true` | | `AreEqualNumerically` of `x ln(x)` and its antiderivative's derivative | `false` | `true` | +### A computed value is a rational only within half the working digits + +**Silent: wrong answers.** A number an evaluation produced became the small rational it was near within a fixed +`1e-16`, whatever the precision, while it became the integer it was near only within half the working digits. So at two +thousand digits `ln(e^a + sqrt(2) e^(a/2) + 1)` was exactly `a`, where it is `a + 1.2e-67`, and the derivative of a right +antiderivative of `1/sqrt(a + i a sinh(c + d x))` at a large argument lost its imaginary part, so that a check by +evaluation read the antiderivative as wrong. A value is now that rational only within half the working digits, as for an +integer. A number from a `double`, a `float` or a `decimal` carries its type's digits, not the working precision's, and +is rounded as before: `(Entity)(1m / 3)` is still `1/3` ([#1944](https://github.com/asc-community/AngouriMath/issues/1944)). + +| Input, at the default hundred digits unless stated | Was (2.6.0) | Now | +|---|---|---| +| `"1/3 + 1e-30".EvalNumerical()` | `1/3` | `0.33333333333333333333333333333433…` | +| `"0.333333333333333333333".EvalNumerical()` | `1/3` | `0.333333333333333333333` | +| `"1.000000000000000000001".EvalNumerical()` | `1` | `1.000000000000000000001` | +| The derivative of the antiderivative of `1/sqrt(a + i*a*sinh(c + d*x))` at `a, c, d, x = 21/10, 19/10, 17/10, 227283/1250`, at 2000 digits | `2.0185464…E-68` | `2.0185464…E-68 - 2.0185464…E-68i` | +| `"1/3 + 1e-60".EvalNumerical()`, `"sin(pi)".EvalNumerical()`, `(Entity)(1.0 / 3)` | `1/3`, `0`, `1/3` | `1/3`, `0`, `1/3` | + --- ## 2.6.0 — since 2.5.0 diff --git a/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs index dee989172..bff929ebf 100644 --- a/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs +++ b/Sources/AngouriMath/Convenience/AngouriMathExtensions.cs @@ -619,7 +619,7 @@ public static Set Solve(this string expr, Variable var) /// 0.100000001490116119384765625 /// /// - public static Real ToNumber(this float value) => Real.Create(EDecimal.FromSingle(value)); + public static Real ToNumber(this float value) => Real.FromMachineNumber(EDecimal.FromSingle(value)); /// /// Converts an into an AM's understandable @@ -639,7 +639,7 @@ public static Set Solve(this string expr, Variable var) /// 1/2 /// /// - public static Real ToNumber(this double value) => Real.Create(EDecimal.FromDouble(value)); + public static Real ToNumber(this double value) => Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// Converts an into an AM's understandable @@ -659,7 +659,7 @@ public static Set Solve(this string expr, Variable var) /// 0.1000000000000000055511151231257827021181583404541015625 /// /// - public static Real ToNumber(this decimal value) => Real.Create(EDecimal.FromDecimal(value)); + public static Real ToNumber(this decimal value) => Real.FromMachineNumber(EDecimal.FromDecimal(value)); /// /// Converts PeterO's into an AM's understandable @@ -695,7 +695,7 @@ public static Set Solve(this string expr, Variable var) /// /// public static Complex ToNumber(this NumericsComplex complex) - => Complex.Create(EDecimal.FromDouble(complex.Real), EDecimal.FromDouble(complex.Imaginary)); + => Complex.FromMachineNumbers(EDecimal.FromDouble(complex.Real), EDecimal.FromDouble(complex.Imaginary)); /// /// Converts an into an AM's understandable diff --git a/Sources/AngouriMath/Convenience/MathS.cs b/Sources/AngouriMath/Convenience/MathS.cs index 1e60171c1..615559fa9 100644 --- a/Sources/AngouriMath/Convenience/MathS.cs +++ b/Sources/AngouriMath/Convenience/MathS.cs @@ -4583,7 +4583,7 @@ public static class Numbers /// /// public static Complex Create(NumericsComplex value) => - Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + Complex.FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// Creates an instance of from a /// A value of type (signed 64-bit integer) @@ -4916,7 +4916,7 @@ public static Rational CreateRational(EInteger numerator, EInteger denominator) /// EDecimal i9 = ((Entity.Number.Real)FromString("1 + 5").EvalNumerical()).EDecimal; /// /// - public static Real Create(double value) => Real.Create(EDecimal.FromDouble(value)); + public static Real Create(double value) => Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// Creates an instance of from two s diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs index ed16435d0..1413254ec 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Complex.Definition.cs @@ -80,18 +80,27 @@ public static Complex Create(Real real, Real imaginary) => /// /// Creates an instance of Complex /// - public static Complex Create(EDecimal real, EDecimal imaginary) + public static Complex Create(EDecimal real, EDecimal imaginary) => Create(real, imaginary, fromMachineNumbers: false); + + /// + /// A number from a pair of s, each part rounded as + /// rounds it. + /// + internal static Complex FromMachineNumbers(EDecimal real, EDecimal imaginary) => Create(real, imaginary, fromMachineNumbers: true); + + private static Complex Create(EDecimal real, EDecimal imaginary, bool fromMachineNumbers) { if (!MathS.Settings.DowncastingEnabled) return new Complex(real, imaginary); + Real Part(EDecimal part) => fromMachineNumbers ? Real.FromMachineNumber(part) : Real.Create(part); if (imaginary.IsZero) - return Real.Create(real); + return Part(real); if (real.IsNaN() || imaginary.IsNaN()) return Real.NaN; if (imaginary.IsFinite && imaginary.Abs().LessThan(MathS.Settings.DowncastingTolerance)) - return Real.Create(real); + return Part(real); else - return new Complex(Real.Create(real), Real.Create(imaginary)); + return new Complex(Part(real), Part(imaginary)); } /// @@ -293,7 +302,7 @@ public static implicit operator Complex(EDecimal value) /// as the value that literal actually holds and not as one tenth. /// public static implicit operator Complex(float value) - => Create(EDecimal.FromSingle(value), 0); + => FromMachineNumbers(EDecimal.FromSingle(value), EDecimal.Zero); /// /// The value as a with no imaginary part, read as the @@ -301,7 +310,7 @@ public static implicit operator Complex(float value) /// written as. /// public static implicit operator Complex(double value) - => Create(EDecimal.FromDouble(value), 0); + => FromMachineNumbers(EDecimal.FromDouble(value), EDecimal.Zero); /// /// The value as a with no imaginary part. A @@ -309,14 +318,14 @@ public static implicit operator Complex(double value) /// that were written. /// public static implicit operator Complex(decimal value) - => Create(EDecimal.FromDecimal(value), 0); + => FromMachineNumbers(EDecimal.FromDecimal(value), EDecimal.Zero); /// /// The .NET complex number as this one, through its two /// parts and their precision. /// public static implicit operator Complex(System.Numerics.Complex value) => - Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// The pair read as real and imaginary parts. public static implicit operator Complex((int re, int im) v) => Create(v.re, v.im); diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs index c7d1a8c7b..adab9d7b3 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Number.Definition.cs @@ -89,26 +89,26 @@ public abstract partial record Number : ContinuousNode /// The value as a , read as the binary it /// is — so 0.1f becomes the value that literal holds, not one tenth. /// - public static implicit operator Number(float value) => Real.Create(EDecimal.FromSingle(value)); + public static implicit operator Number(float value) => Real.FromMachineNumber(EDecimal.FromSingle(value)); /// /// The value as a , read as the binary it /// is rather than as the decimal it was written as. /// - public static implicit operator Number(double value) => Real.Create(EDecimal.FromDouble(value)); + public static implicit operator Number(double value) => Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// The value as a . A is decimal /// already, so this one keeps the digits that were written. /// - public static implicit operator Number(decimal value) => Real.Create(EDecimal.FromDecimal(value)); + public static implicit operator Number(decimal value) => Real.FromMachineNumber(EDecimal.FromDecimal(value)); /// /// The .NET complex number as a , through its two /// parts and their precision. /// public static implicit operator Number(System.Numerics.Complex value) - => Complex.Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + => Complex.FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// The big integer as an , exactly and at any size. /// /// Read as the two's-complement bytes it is, which is what diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs index 727ba11d4..aaaf8c0c4 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Rational.Definition.cs @@ -162,6 +162,13 @@ public static Rational Create(ERational value) /// if found, otherwise. /// public static Rational? FindRational(EDecimal num, int iterCount = int.MinValue) + => FindRationalWithin(num, MathS.Settings.PrecisionErrorZeroRange, iterCount); + + /// + /// , accepting a level of the continued fraction whose + /// remainder is below its integer part times . + /// + internal static Rational? FindRationalWithin(EDecimal num, EDecimal tolerance, int iterCount = int.MinValue) { if (iterCount is int.MinValue) iterCount = MathS.Settings.FloatToRationalIterCount; @@ -179,11 +186,11 @@ public static Rational Create(ERational value) // the exact one. if (!MayBeASmallRational(num, iterCount)) return null; - return FindRationalExactly(num, iterCount); + return FindRationalExactly(num, iterCount, tolerance); } /// The continued fraction in the decimal's own arithmetic, level by level. - internal static Rational? FindRationalExactly(EDecimal num, int iterCount) + internal static Rational? FindRationalExactly(EDecimal num, int iterCount, EDecimal tolerance) { if (iterCount <= 0) return null; @@ -200,12 +207,12 @@ public static Rational Create(ERational value) // 1e-20 came to parse as 0. Below the top, intPart is at least 1 (rest // was inverted to get here), so the test stays as permissive as it was. if (rest.Abs().LessThan(EDecimal.FromEInteger(intPart) - .Multiply(MathS.Settings.PrecisionErrorZeroRange, MathS.Settings.DecimalPrecisionContext))) + .Multiply(tolerance, MathS.Settings.DecimalPrecisionContext))) return Integer.Create(sign * intPart); else { var inv = CtxDivide(EDecimal.One, rest); - var rat = FindRationalExactly(inv, iterCount - 1); + var rat = FindRationalExactly(inv, iterCount - 1, tolerance); if (rat is null) return null; return new Rational((intPart * sign + sign / rat.ERational).InLowestTerms()); @@ -223,8 +230,8 @@ public static Rational Create(ERational value) /// /// /// The exact search accepts a level whose remainder is below the integer part - /// times , ten to the minus - /// sixteen. In a double the remainder at a level carries an error of the value's + /// times its tolerance, at most , + /// ten to the minus sixteen, unless a caller has set it higher. In a double the remainder at a level carries an error of the value's /// representation error, two to the minus fifty-two, times the square of that /// level's convergent denominator; the tolerance here is the exact one widened by /// that bound, so a value the exact search would accept is never refused, and diff --git a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs index 822c44781..ba6d381cb 100644 --- a/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs +++ b/Sources/AngouriMath/Core/Entity/Continuous/Entity.Continuous.Real.Definition.cs @@ -58,7 +58,26 @@ private protected Real() : base(null, null) { } /// Creates an instance of Real /// (one can do it by implicit conversation) /// - public static Real Create(EDecimal value) + /// + /// A value is the small rational it is near within half the working digits, as it is the integer it + /// is near. Within a fixed 1e-16, ln(e^a + sqrt(2) e^(a/2) + 1) was exactly a at two + /// thousand digits, where it is a + 1.2e-67, and a derivative built on the difference of two + /// such logarithms lost its imaginary part. + /// https://github.com/asc-community/AngouriMath/issues/1944 + /// + public static Real Create(EDecimal value) => Create(value, fromMachineNumber: false); + + /// + /// A number from a , a or a , which carries + /// that type's digits and not the working precision's: it is the small rational it is near within + /// , ten to the minus sixteen, so that 1m/3 + /// is 1/3. https://github.com/asc-community/AngouriMath/issues/1944 + /// + internal static Real FromMachineNumber(EDecimal value) => Create(value, fromMachineNumber: true); + + // The tolerance is made only past the cheap check below, which almost every value stops at: made at + // the call, it was 580 more bytes on each evaluation of a cubic at a hundred digits. + private static Real Create(EDecimal value, bool fromMachineNumber) { if (!MathS.Settings.DowncastingEnabled) return new Real(value); @@ -79,7 +98,7 @@ public static Real Create(EDecimal value) if (intRest.GreaterThan(1 - tolerance)) return Integer.Create(intPart.Increment()); - var attempt = Rational.FindRational(value); + var attempt = Rational.FindRationalWithin(value, fromMachineNumber ? MathS.Settings.PrecisionErrorZeroRange : tolerance); if (attempt is null || attempt.ERational.Numerator.Abs() > MathS.Settings.MaxAbsNumeratorOrDenominatorValue || attempt.ERational.Denominator.Abs() > MathS.Settings.MaxAbsNumeratorOrDenominatorValue) @@ -279,19 +298,19 @@ internal static bool TryParse(string s, /// The value as a , read as the binary /// it is — so 0.1f becomes the value that literal holds, not one tenth. /// - public static implicit operator Real(float value) => Create(EDecimal.FromSingle(value)); + public static implicit operator Real(float value) => FromMachineNumber(EDecimal.FromSingle(value)); /// /// The value as a , read as the binary /// it is rather than as the decimal it was written as. /// - public static implicit operator Real(double value) => Create(EDecimal.FromDouble(value)); + public static implicit operator Real(double value) => FromMachineNumber(EDecimal.FromDouble(value)); /// /// The value as a . A is decimal /// already, so this one keeps the digits that were written. /// - public static implicit operator Real(decimal value) => Create(EDecimal.FromDecimal(value)); + public static implicit operator Real(decimal value) => FromMachineNumber(EDecimal.FromDecimal(value)); } } } diff --git a/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs b/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs index d0c44e4b6..b9de52b31 100644 --- a/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs +++ b/Sources/AngouriMath/Core/Entity/Entity.ImplicitOperators.cs @@ -58,26 +58,26 @@ partial record Entity /// and not one tenth. Write 0.1m, or a , for the exact /// number. /// - public static implicit operator Entity(float value) => Number.Real.Create(EDecimal.FromSingle(value)); + public static implicit operator Entity(float value) => Number.Real.FromMachineNumber(EDecimal.FromSingle(value)); /// /// The value as a literal, read as the binary /// it is rather than as the decimal it was written as. /// - public static implicit operator Entity(double value) => Number.Real.Create(EDecimal.FromDouble(value)); + public static implicit operator Entity(double value) => Number.Real.FromMachineNumber(EDecimal.FromDouble(value)); /// /// The value as a literal. A is /// decimal already, so this one keeps the digits that were written. /// - public static implicit operator Entity(decimal value) => Number.Real.Create(EDecimal.FromDecimal(value)); + public static implicit operator Entity(decimal value) => Number.Real.FromMachineNumber(EDecimal.FromDecimal(value)); /// /// The .NET complex number as a literal, through its two /// parts and their precision. /// public static implicit operator Entity(System.Numerics.Complex value) => - Number.Complex.Create(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); + Number.Complex.FromMachineNumbers(EDecimal.FromDouble(value.Real), EDecimal.FromDouble(value.Imaginary)); /// The big integer as an literal, exactly. public static implicit operator Entity(System.Numerics.BigInteger bigInt) diff --git a/Sources/Tests/UnitTests/Core/AValueKeepsTheDigitsItHasTest.cs b/Sources/Tests/UnitTests/Core/AValueKeepsTheDigitsItHasTest.cs new file mode 100644 index 000000000..0bd187928 --- /dev/null +++ b/Sources/Tests/UnitTests/Core/AValueKeepsTheDigitsItHasTest.cs @@ -0,0 +1,79 @@ +// +// Copyright (c) 2019-2026 Angouri. +// AngouriMath is licensed under MIT. +// Details: https://github.com/asc-community/AngouriMath/blob/master/LICENSE.md. +// Website: https://am.angouri.org. +// + +using AngouriMath.Extensions; +using PeterO.Numbers; +using Xunit; +using static AngouriMath.Entity.Number; + +namespace AngouriMath.Tests.Core +{ + /// + /// A numerical evaluation rounds a value onto a small rational only within half the working digits, as it + /// rounds onto an integer, where it rounded within 1e-16 whatever the precision: at two thousand digits the + /// derivative of a right antiderivative of 1/sqrt(a + i a sinh(c + d x)) lost its imaginary part that + /// way. The residuals of exact cancellation are still exact, and so are numbers from a double or a decimal. + /// The reference is mpmath's at 120 digits. + /// #1944 + /// + [Trait("Area", "Core")] + public sealed class AValueKeepsTheDigitsItHasTest + { + private static void AssertClose(string reference, EDecimal actual) + { + var expected = EDecimal.FromString(reference); + var error = actual.Subtract(expected, EContext.ForPrecision(200)).Abs().Divide(expected.Abs(), EContext.ForPrecision(10)); + Assert.True(error.LessThan(EDecimal.FromString("1E-40")), $"{actual} where it is {expected}"); + } + + [Theory] + [InlineData("1/3 + 1e-30")] + [InlineData("0.333333333333333333333")] + public void AValueNearARationalIsNotThatRational(string expression) + => Assert.IsType(expression.EvalNumerical()); + + [Fact] + public void AMachineNumberIsTheRationalItIsNearToItsOwnDigits() + { + // A double or a decimal carries its type's digits, not the working precision's, and is read as + // before: within 1e-16 of a small rational, it is that rational. + Assert.Equal((Entity)Rational.Create(1, 3), (Entity)(1m / 3)); + Assert.Equal((Entity)Rational.Create(1, 3), (Entity)(1.0 / 3)); + Assert.Equal((Entity)Rational.Create(1, 4), (Entity)0.25); + } + + [Theory] + [InlineData("sin(pi)", "0")] + [InlineData("cos(pi/2)", "0")] + [InlineData("e^(i*pi) + 1", "0")] + [InlineData("sin(100*pi)", "0")] + [InlineData("sqrt(2)*sqrt(3) - sqrt(6)", "0")] + [InlineData("sin(pi/6)", "1/2")] + [InlineData("sqrt(2)^2/3", "2/3")] + [InlineData("gamma(1/2)^2/pi", "1")] + public void AResidualOfExactArithmeticIsStillExact(string expression, string exact) + => Assert.Equal(exact.ToEntity(), expression.EvalNumerical()); + + [Fact] + public void WithinHalfTheDigitsAValueIsTheRationalItIsNear() + => Assert.Equal(Rational.Create(1, 3), "1/3 + 1e-60".EvalNumerical()); + + [Fact] + public void TheDerivativeOfAnAntiderivativeAtALargeHyperbolicArgumentIsTheIntegrand() + { + const int digits = 2000; + var integrand = "1/sqrt(a + i*a*sinh(c + d*x))".ToEntity(); + var derivative = integrand.Integrate("x").Differentiate("x"); + Entity Pin(Entity e) => e.Substitute("a", Rational.Create(21, 10)).Substitute("c", Rational.Create(19, 10)) + .Substitute("d", Rational.Create(17, 10)).Substitute("x", Rational.Create(227283, 1250)); + using var _ = MathS.Settings.DecimalPrecisionContext.Set(new EContext(digits, ERounding.HalfEven, -100000, 100000, false)); + var value = Assert.IsType(Pin(derivative).EvalNumerical()); + AssertClose("2.01854640568606010538100888350636282143172892E-68", value.RealPart.EDecimal); + AssertClose("-2.01854640568606010538100888350636282143172892E-68", value.ImaginaryPart.EDecimal); + } + } +} diff --git a/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs b/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs index 5f1a7efad..71328d9bc 100644 --- a/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs +++ b/Sources/Tests/UnitTests/Core/HighPrecisionFunctionsTest.cs @@ -153,7 +153,7 @@ public void TheCheapDecisionAgreesWithTheExactSearch() var checkedCount = 0; void Check(EDecimal value) { - var exact = Rational.FindRationalExactly(value, iterCount); + var exact = Rational.FindRationalExactly(value, iterCount, MathS.Settings.PrecisionErrorZeroRange); var cheap = Rational.MayBeASmallRational(value, iterCount); var found = Rational.FindRational(value); checkedCount++; @@ -216,7 +216,7 @@ public void TheCheapDecisionReadsTwoThousandDigits() var denominator = 1 + random.Next(100_000_000); var numerator = random.Next(100_000_000); var value = EDecimal.FromInt32(numerator).Divide(EDecimal.FromInt32(denominator), context); - var exact = Rational.FindRationalExactly(value, iterCount); + var exact = Rational.FindRationalExactly(value, iterCount, MathS.Settings.PrecisionErrorZeroRange); if (exact is not null) Assert.True(Rational.MayBeASmallRational(value, iterCount)); Assert.Equal(exact, Rational.FindRational(value)); diff --git a/Sources/Tests/UnitTests/Core/Numeric.cs b/Sources/Tests/UnitTests/Core/Numeric.cs index 7710e0097..fd165c123 100644 --- a/Sources/Tests/UnitTests/Core/Numeric.cs +++ b/Sources/Tests/UnitTests/Core/Numeric.cs @@ -433,8 +433,10 @@ public void AComplexDivisionWithALargeDivisor(string ar, string ai, string br, s /// /// The modulus is made under the downcasting setting in force. It turned the downcasting on /// for itself, so with it off |75 + 316.22776601683796i|, which is 325 + 2.6e-17, - /// came back as 325. With it on, 325 is the setting's own rounding. + /// came back as 325. With it on, the setting rounds a value to half the working digits, which + /// 2.6e-17 is far above, so it is the same value; a modulus that is 325 to the working digits is 325. /// #1615 + /// #1944 /// [Fact] public void AModulusIsMadeUnderTheDowncastingInForce() @@ -449,7 +451,12 @@ public void AModulusIsMadeUnderTheDowncastingInForce() Assert.True(evaluated.RealPart.EDecimal.Subtract(expected).Abs().CompareTo(EDecimal.Create(1, -90)) < 0, $"{evaluated}"); } using (MathS.Settings.DowncastingEnabled.Set(true)) - Assert.Equal(Integer.Create(325), Complex.Create(EDecimal.FromInt32(75), EDecimal.FromString("316.22776601683796")).Abs()); + { + var modulus = Complex.Create(EDecimal.FromInt32(75), EDecimal.FromString("316.22776601683796")).Abs(); + Assert.True(modulus.EDecimal.Subtract(expected).Abs().CompareTo(EDecimal.Create(1, -90)) < 0, $"{modulus}"); + var rootOf100000 = EDecimal.FromInt32(100000).Sqrt(MathS.Settings.DecimalPrecisionContext); + Assert.Equal(Integer.Create(325), Complex.Create(EDecimal.FromInt32(75), rootOf100000).Abs()); + } } } }