diff --git a/.gitignore b/.gitignore index dc0470a..e043c9f 100644 --- a/.gitignore +++ b/.gitignore @@ -203,6 +203,11 @@ PublishScripts/ **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ +# and except a Unity project's Packages/, which is source: Unity's package manifest and its +# resolved lock file are both meant to be committed, and a NuGet restore folder never contains +# a file by either name. +!**/[Pp]ackages/manifest.json +!**/[Pp]ackages/packages-lock.json # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files @@ -651,3 +656,16 @@ Temporary Items # ImGui.ini files imgui.ini + +# Game engine projects +# +# Godot: the import cache, and the mono/temp bin+obj a C# build writes. +.godot/ + +# Unity: .meta files are source, not the Visual Studio C++ build artifact that the `*.meta` rule +# further up targets. Unity generates one per asset and it carries the GUID that scenes, prefabs +# and serialized references point at, so ignoring them gives every clone fresh GUIDs and silently +# breaks those references - including for a plug-in whose .dll is itself a build output. This +# negation has to come after that rule to win, and is scoped to the asset tree so the Visual +# Studio artifact stays ignored everywhere else. +!**/[Aa]ssets/**/*.meta diff --git a/PreciseNumber.Test/PreciseNumberTests.cs b/PreciseNumber.Test/PreciseNumberTests.cs index c200c19..7bb862f 100644 --- a/PreciseNumber.Test/PreciseNumberTests.cs +++ b/PreciseNumber.Test/PreciseNumberTests.cs @@ -2055,6 +2055,48 @@ public void TestDivideIsExactWhenTheQuotientTerminates() } } + [TestMethod] + public void TestDivideIsExactWhenTheDenominatorOnlyTerminatesAfterReduction() + { + // The denominators here carry factors that are neither two nor five, so they only + // terminate once the numerator has cancelled them: 91/7 is 13, 6/3 is 2, 22/11 is 2. + // Asking for fewer significant digits than the exact answer needs is what exposes it - + // at the default precision the rounded path happens to land on the same value. + (int Numerator, int Denominator, string Expected)[] cases = + [ + (91, 7, "13"), + (6, 3, "2"), + (22, 11, "2"), + (-91, 7, "-13"), + (91, -7, "-13"), + (126, 14, "9"), + (1001, 7, "143"), + ]; + + foreach ((int numerator, int denominator, string expected) in cases) + { + PreciseNumber quotient = PreciseNumber.Divide( + numerator.ToPreciseNumber(), + denominator.ToPreciseNumber(), + 1); + + Assert.AreEqual(expected, quotient.ToString(CultureInfo.InvariantCulture), $"{numerator}/{denominator} at 1 significant digit"); + } + } + + [TestMethod] + public void TestDivideStillRoundsWhenReductionLeavesANonTerminatingDenominator() + { + // Reducing must not be mistaken for terminating: 14/6 reduces to 7/3, whose denominator + // still has a factor of three, so the quotient repeats and the requested precision applies. + PreciseNumber fourteen = 14.ToPreciseNumber(); + PreciseNumber six = 6.ToPreciseNumber(); + + Assert.AreEqual("2", PreciseNumber.Divide(fourteen, six, 1).ToString(CultureInfo.InvariantCulture)); + Assert.AreEqual("2.33", PreciseNumber.Divide(fourteen, six, 3).ToString(CultureInfo.InvariantCulture)); + Assert.AreEqual("2.3333", PreciseNumber.Divide(fourteen, six, 5).ToString(CultureInfo.InvariantCulture)); + } + [TestMethod] public void TestDivideKeepsEveryDigitOfALongTerminatingQuotient() { diff --git a/PreciseNumber/PreciseNumber.cs b/PreciseNumber/PreciseNumber.cs index e1741cb..5971c33 100644 --- a/PreciseNumber/PreciseNumber.cs +++ b/PreciseNumber/PreciseNumber.cs @@ -1484,9 +1484,21 @@ public static PreciseNumber Divide(PreciseNumber left, PreciseNumber right, int /// true if the quotient terminates and is exact; otherwise false. private static bool TryDivideExactly(BigInteger numerator, BigInteger denominator, int exponent, out PreciseNumber result) { - // A fraction terminates in base ten exactly when its denominator is 2^twos * 5^fives. Most - // denominators are rejected by the first remainder test, which is why this is worth trying - // before falling back to a rounded quotient. + // A fraction terminates in base ten exactly when its *reduced* denominator is + // 2^twos * 5^fives, so reduce before factorizing: a denominator factor that cancels against + // the numerator, as the seven of 91/7 does, leaves a terminating quotient behind and must + // not keep this off the exact path. The numerator is non-zero here - Divide returns early + // for a zero dividend - so the greatest common divisor is positive and the denominator + // stays positive across the reduction. + BigInteger common = BigInteger.GreatestCommonDivisor(BigInteger.Abs(numerator), denominator); + if (!common.IsOne) + { + numerator /= common; + denominator /= common; + } + + // Most denominators are rejected by the first remainder test, which is why this is worth + // trying before falling back to a rounded quotient. int twos = (int)BigInteger.TrailingZeroCount(denominator); BigInteger remaining = denominator >> twos;