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
100 changes: 100 additions & 0 deletions SignificantNumber.Test/SignificantNumberExactOperandTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace SignificantNumber.Test;

using System.Globalization;
using System.Numerics;
using ktsu.SignificantNumber;

/// <summary>
/// Covers arithmetic and comparison where both operands are -1, 0, or 1, which have unlimited precision.
/// </summary>
[TestClass]
public class SignificantNumberExactOperandTests
{
private static SignificantNumber Parse(string text) =>
SignificantNumber.Parse(text, NumberStyles.Float, CultureInfo.InvariantCulture);

private static T Square<T>(T value)
where T : INumber<T> =>
value * value;

[TestMethod]
[DataRow("1", "0")]
[DataRow("-1", "0")]
[DataRow("0", "0")]
[DataRow("0", "1")]
[DataRow("0", "-1")]
public void Multiply_ByZero_ReturnsZero(string left, string right) =>
Assert.AreEqual(SignificantNumber.Zero, Parse(left) * Parse(right));

[TestMethod]
public void Multiply_DefaultByDefault_ReturnsZero() =>
Assert.AreEqual(SignificantNumber.Zero, default(SignificantNumber) * default(SignificantNumber));

[TestMethod]
public void Square_OfZeroThroughGenericMath_ReturnsZero() =>
Assert.AreEqual(SignificantNumber.Zero, Square(SignificantNumber.Zero));

[TestMethod]
public void Multiply_ExactOperands_IsCommutativeAndExact()
{
string[] values = ["-1", "0", "1"];
foreach (string left in values)
{
foreach (string right in values)
{
SignificantNumber expected = Parse((int.Parse(left, CultureInfo.InvariantCulture) * int.Parse(right, CultureInfo.InvariantCulture)).ToString(CultureInfo.InvariantCulture));
Assert.AreEqual(expected, Parse(left) * Parse(right), $"{left} * {right}");
Assert.AreEqual(Parse(right) * Parse(left), Parse(left) * Parse(right), $"{left} * {right} vs {right} * {left}");
}
}
}

[TestMethod]
[DataRow("0", "1", "0")]
[DataRow("0", "-1", "0")]
[DataRow("1", "-1", "-1")]
[DataRow("-1", "-1", "1")]
public void Divide_ExactOperands_ReturnsExactQuotient(string left, string right, string expected) =>
Assert.AreEqual(Parse(expected), Parse(left) / Parse(right));

[TestMethod]
[DataRow("0", "1")]
[DataRow("1", "1")]
[DataRow("-1", "1")]
[DataRow("0", "-1")]
public void Mod_ExactOperands_ReturnsZero(string left, string right) =>
Assert.AreEqual(SignificantNumber.Zero, Parse(left) % Parse(right));

[TestMethod]
public void CompareTo_ExactOperands_IsAntisymmetric()
{
string[] values = ["-1", "0", "1"];
foreach (string left in values)
{
foreach (string right in values)
{
int expected = int.Parse(left, CultureInfo.InvariantCulture).CompareTo(int.Parse(right, CultureInfo.InvariantCulture));
Assert.AreEqual(expected, int.Sign(Parse(left).CompareTo(Parse(right))), $"{left}.CompareTo({right})");
}
}
}

[TestMethod]
[DataRow("1", "1.23", "1.23")]
[DataRow("-1", "1.23", "-1.23")]
[DataRow("1.23", "1", "1.23")]
[DataRow("1.23", "-1", "-1.23")]
[DataRow("0", "1.23", "0")]
[DataRow("1.23", "0", "0")]
public void Multiply_OneExactOperand_KeepsTheOtherOperandsPrecision(string left, string right, string expected) =>
Assert.AreEqual(Parse(expected), Parse(left) * Parse(right));

[TestMethod]
[DataRow("1.23", "1", "1.23")]
[DataRow("1.23", "-1", "-1.23")]
[DataRow("1", "4.1", "0.24")]
public void Divide_OneExactOperand_KeepsTheOtherOperandsPrecision(string left, string right, string expected) =>
Assert.AreEqual(Parse(expected), Parse(left) / Parse(right));
}
29 changes: 21 additions & 8 deletions SignificantNumber/SignificantNumber.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,18 +178,31 @@ private static int LowestDecimalDigits(PreciseNumber left, PreciseNumber right)
/// </summary>
/// <param name="left">The first number.</param>
/// <param name="right">The second number.</param>
/// <returns>The lower of the significant digit counts of the two numbers.</returns>
/// <returns>
/// The lower of the significant digit counts of the two numbers, or 1 when both have unlimited precision, since any
/// product, quotient, or remainder of -1, 0, and 1 is exact at one significant digit.
/// </returns>
private static int LowestSignificantDigits(PreciseNumber left, PreciseNumber right)
{
int leftSignificantDigits = left.SignificantDigits;
int rightSignificantDigits = right.SignificantDigits;
bool leftIsExact = HasInfinitePrecision(left);
bool rightIsExact = HasInfinitePrecision(right);

if (leftIsExact && rightIsExact)
{
return 1;
}

leftSignificantDigits = HasInfinitePrecision(left) ? rightSignificantDigits : leftSignificantDigits;
rightSignificantDigits = HasInfinitePrecision(right) ? leftSignificantDigits : rightSignificantDigits;
if (leftIsExact)
{
return right.SignificantDigits;
}

if (rightIsExact)
{
return left.SignificantDigits;
}

return leftSignificantDigits < rightSignificantDigits
? leftSignificantDigits
: rightSignificantDigits;
return int.Min(left.SignificantDigits, right.SignificantDigits);
}

/// <summary>
Expand Down
Loading