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/SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs b/SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs index d3b5777..dafb95d 100644 --- a/SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs +++ b/SignificantNumber.Test/SignificantNumberPowExpPrecisionTests.cs @@ -49,4 +49,16 @@ public void Exp_PowerNearSevenHundred_ReportsTwelveSignificantDigits() => [TestMethod] public void Pow_SmallLogarithmOfResult_KeepsTheDoublesPrecision() => Assert.AreEqual(15, Parse("1.2345678901234567890").Pow(Precise("0.5000000000000000001")).SignificantDigits); + + // A base in the subnormal double range carries only a few digits as a double, so its logarithm must come + // from its significand and exponent. Reference values from Python decimal at 60 digits. + [TestMethod] + [DataRow("1.23456789e-320", "0.5000001", "1.11102926279157148966176568132872269505814553044809283401739E-160")] + [DataRow("1.23456789e-320", "-0.5000001", "9.00066302022865331618424814993882055897651460128454545426800E+159")] + public void Pow_SubnormalDoubleBase_ReportsOnlyCorrectDigits(string baseNumber, string power, string trueValue) + { + SignificantNumber result = Parse(baseNumber).Pow(Precise(power)); + Assert.AreEqual(7, result.SignificantDigits); + AssertReportedDigitsAreCorrect(trueValue, result); + } } diff --git a/SignificantNumber/SignificantNumber.cs b/SignificantNumber/SignificantNumber.cs index 9179e20..f8b0115 100644 --- a/SignificantNumber/SignificantNumber.cs +++ b/SignificantNumber/SignificantNumber.cs @@ -652,7 +652,12 @@ private static SignificantNumber PowByLogarithm(PreciseNumber value, PreciseNumb // |x|^p = e^(p × ln|x|), so the digits are bounded by how precisely e^(p × ln|x|) is known. significantDigits = CapToExponentPrecision(significantDigits, Log10OfMagnitude(value) * Math.Log(10) * power.To()); - double logValue = Math.Log(Math.Abs(value.To())); + // A base outside the normal double range (subnormal, or beyond double.MaxValue) loses digits or becomes + // infinite as a double, so its logarithm is built from its significand and exponent instead. + double baseMagnitude = Math.Abs(value.To()); + double logValue = double.IsNormal(baseMagnitude) + ? Math.Log(baseMagnitude) + : Log10OfMagnitude(value) * Math.Log(10); double magnitude = Math.Exp(logValue * power.To()); if (double.IsNormal(magnitude)) {