Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
58 changes: 58 additions & 0 deletions PreciseNumber.Test/PreciseNumberTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,64 @@ public void TestGetHashCode()
Assert.AreEqual(negativeOne.GetHashCode(), PreciseNumber.NegativeOne.GetHashCode());
}

[TestMethod]
public void TestGetHashCodeAgreesWithEqualsForUnsanitizedValues()
{
// Equals compares numerically, so an un-sanitized 2.50 equals a sanitized 2.5. The hash
// has to agree, or the two land in different buckets of a dictionary and lookups miss.
(int Exponent, int Significand)[] unsanitized =
[
(-2, 250), // 2.50
(-3, 2500), // 2.500
(0, 100), // 100
(1, 10), // 100
(-1, -250), // -25.0
(0, 0), // zero
(5, 0), // zero, at a non-zero exponent
];

foreach ((int exponent, int significand) in unsanitized)
{
PreciseNumber raw = PreciseNumber.CreateFromComponents(exponent, significand, sanitize: false);
PreciseNumber sanitized = PreciseNumber.CreateFromComponents(exponent, significand);

Assert.IsTrue(raw.Equals(sanitized), $"({exponent}, {significand}) should equal its sanitized form");
Assert.AreEqual(
sanitized.GetHashCode(),
raw.GetHashCode(),
$"({exponent}, {significand}) is Equals-equal to its sanitized form, so it must hash alike");
}
}

[TestMethod]
public void TestGetHashCodeAgreesWithEqualsAfterCommonizing()
{
// MakeCommonized hands back un-normalized values directly, which is the other way a
// caller can reach an instance whose stored fields carry trailing zeros.
PreciseNumber twoAndAHalf = PreciseNumber.CreateFromComponents(-1, 25);
PreciseNumber thousandths = PreciseNumber.CreateFromComponents(-3, 1);

(PreciseNumber commonized, PreciseNumber _) = PreciseNumber.MakeCommonized(twoAndAHalf, thousandths);

Assert.IsTrue(commonized.Equals(twoAndAHalf));
Assert.AreEqual(twoAndAHalf.GetHashCode(), commonized.GetHashCode());
}

[TestMethod]
public void TestUnsanitizedValuesShareADictionarySlotWithTheirSanitizedForm()
{
// The contract violation as a caller meets it: a lookup that silently misses.
Dictionary<PreciseNumber, string> lookup = new()
{
[PreciseNumber.CreateFromComponents(-1, 25)] = "two and a half",
};

PreciseNumber raw = PreciseNumber.CreateFromComponents(-2, 250, sanitize: false);

Assert.IsTrue(lookup.TryGetValue(raw, out string? found), "2.50 should find the entry stored under 2.5");
Assert.AreEqual("two and a half", found);
}

[TestMethod]
public void TestEqualsObjectSameInstance()
{
Expand Down
26 changes: 25 additions & 1 deletion PreciseNumber/PreciseNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -450,7 +450,31 @@
Equal(this, other);

/// <inheritdoc/>
public override int GetHashCode() => HashCode.Combine(Exponent, Significand);
/// <remarks>
/// Hashes the sanitized form rather than the stored fields. <see cref="Equals(PreciseNumber)"/>
/// compares numerically, so an un-sanitized 2.50 - significand 250 at exponent -2, reachable
/// through <see cref="CreateFromComponents(int, BigInteger, bool)"/> and the commonizing
/// helpers - equals a sanitized 2.5 and has to hash alike, or the two land in different buckets
/// of a hash table and lookups silently miss.
/// </remarks>
public override int GetHashCode()
{
if (Significand.IsZero)
{
// Every zero is equal, whatever exponent it happens to be stored at.
return HashCode.Combine(0, BigInteger.Zero);
}

// The leading digit is non-zero, so at most SignificantDigits - 1 zeros can trail.
int trailingZeros = CountTrailingZeros(Significand, SignificantDigits - 1);

// Unchecked because a hash must not throw. The sanitizing constructor does this addition
// checked, so an exponent that could overflow here cannot survive ordinary arithmetic
// anyway, and a wrapped value still hashes Equals-equal pairs to the same bucket.
return trailingZeros == 0
? HashCode.Combine(Exponent, Significand)
: HashCode.Combine(unchecked(Exponent + trailingZeros), Significand / Pow10(trailingZeros));
}

/// <inheritdoc/>
public override string ToString() => ToString(this, null, null);
Expand Down Expand Up @@ -673,7 +697,7 @@
/// A fixed precision such as <c>E15</c> rounds values that need 17 digits, which turns
/// <see cref="double.MaxValue"/> into a number that converts back to infinity.
/// </remarks>
internal static string GetStringFormatForFloatType<TFloat>()

Check warning on line 700 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 700 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 700 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 700 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 700 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this method and declare a constant for this value.

Check warning on line 700 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Remove this method and declare a constant for this value.
where TFloat : INumber<TFloat>
=> "R";

Expand Down Expand Up @@ -1087,7 +1111,7 @@
public static PreciseNumber MinMagnitudeNumber(PreciseNumber x, PreciseNumber y) => MinMagnitude(x, y);

/// <inheritdoc/>
public static PreciseNumber Parse(ReadOnlySpan<char> s, NumberStyles style, IFormatProvider? provider)

Check warning on line 1114 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

Check warning on line 1114 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

Check warning on line 1114 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.

Check warning on line 1114 in PreciseNumber/PreciseNumber.cs

View workflow job for this annotation

GitHub Actions / Analyze & Release

Refactor this method to reduce its Cognitive Complexity from 23 to the 15 allowed.
{
if (s.IsEmpty)
{
Expand Down
Loading