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
26 changes: 22 additions & 4 deletions Semantics.Quantities/Vector0Guards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ public static class Vector0Guards
/// <param name="value">The value (already converted to the SI base unit) to validate.</param>
/// <param name="paramName">Name of the originating parameter, used for the exception message.</param>
/// <returns>The validated, non-negative value.</returns>
/// <exception cref="ArgumentException">When <paramref name="value"/> is negative.</exception>
/// <exception cref="ArgumentException">When <paramref name="value"/> is NaN or negative.</exception>
public static T EnsureNonNegative<T>(T value, string paramName)
where T : struct, INumber<T>
{
if (T.Sign(value) < 0)
EnsureNumber(value, paramName);

if (value < T.Zero)
{
throw new ArgumentException(
$"Magnitude must be non-negative; received {value}.",
Expand All @@ -53,11 +55,13 @@ public static T EnsureNonNegative<T>(T value, string paramName)
/// <param name="value">The value (already converted to the SI base unit) to validate.</param>
/// <param name="paramName">Name of the originating parameter, used for the exception message.</param>
/// <returns>The validated, strictly-positive value.</returns>
/// <exception cref="ArgumentException">When <paramref name="value"/> is zero or negative.</exception>
/// <exception cref="ArgumentException">When <paramref name="value"/> is NaN, zero or negative.</exception>
public static T EnsurePositive<T>(T value, string paramName)
where T : struct, INumber<T>
{
if (T.Sign(value) <= 0)
EnsureNumber(value, paramName);

if (value <= T.Zero)
{
throw new ArgumentException(
$"Value must be strictly positive; received {value}.",
Expand All @@ -66,4 +70,18 @@ public static T EnsurePositive<T>(T value, string paramName)

return value;
}

/// <summary>
/// Throws <see cref="ArgumentException"/> when <paramref name="value"/> is NaN. Checked before the
/// sign test because <c>T.Sign(NaN)</c> throws <see cref="ArithmeticException"/> for the IEEE types,
/// and a NaN compares false against zero, so neither test rejects it on its own.
/// </summary>
private static void EnsureNumber<T>(T value, string paramName)
where T : struct, INumber<T>
{
if (T.IsNaN(value))
{
throw new ArgumentException("Magnitude must be a number; received NaN.", paramName);
}
}
}
34 changes: 34 additions & 0 deletions Semantics.Test/Quantities/Vector0InvariantTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ namespace ktsu.Semantics.Test.Quantities;
/// <item><description>Issue #50: factories reject negative inputs with <see cref="ArgumentException"/>.</description></item>
/// <item><description>Issue #52: V0 - V0 returns the same V0 of <c>T.Abs(a - b)</c>.</description></item>
/// <item><description>Issue #285: a V0 has no unary negation, and scaling it by a negative number throws.</description></item>
/// <item><description>Issue #326: a NaN input is rejected with <see cref="ArgumentException"/>, not <see cref="ArithmeticException"/>.</description></item>
/// </list>
/// </summary>
[TestClass]
Expand Down Expand Up @@ -332,4 +333,37 @@ public void Vector0_Scaled_By_NonNegative_Scalar_Is_Allowed()
[TestMethod]
public void Vector1_Scaled_By_Negative_Scalar_Is_Signed()
=> Assert.AreEqual(-6.0, (Velocity1D<double>.FromMeterPerSecond(2.0) * -3.0).Value, Tolerance);

// =========================================================== #326: NaN is an ArgumentException

[TestMethod]
public void EnsureNonNegative_NaN_Throws_ArgumentException()
{
_ = Assert.ThrowsExactly<ArgumentException>(() => Vector0Guards.EnsureNonNegative(double.NaN, "value"));
_ = Assert.ThrowsExactly<ArgumentException>(() => Vector0Guards.EnsureNonNegative(float.NaN, "value"));
_ = Assert.ThrowsExactly<ArgumentException>(() => Vector0Guards.EnsureNonNegative(Half.NaN, "value"));
}

[TestMethod]
public void EnsurePositive_NaN_Throws_ArgumentException()
{
_ = Assert.ThrowsExactly<ArgumentException>(() => Vector0Guards.EnsurePositive(double.NaN, "value"));
_ = Assert.ThrowsExactly<ArgumentException>(() => Vector0Guards.EnsurePositive(float.NaN, "value"));
_ = Assert.ThrowsExactly<ArgumentException>(() => Vector0Guards.EnsurePositive(Half.NaN, "value"));
}

[TestMethod]
public void Generated_Factories_NaN_Throw_ArgumentException()
{
_ = Assert.ThrowsExactly<ArgumentException>(() => Speed<double>.FromMeterPerSecond(double.NaN));
_ = Assert.ThrowsExactly<ArgumentException>(() => Mass<double>.FromKilogram(double.NaN));
_ = Assert.ThrowsExactly<ArgumentException>(() => Wavelength<double>.FromMeter(double.NaN));
}

[TestMethod]
public void Guards_Accept_Infinity_And_Reject_NegativeInfinity()
{
Assert.AreEqual(double.PositiveInfinity, Vector0Guards.EnsurePositive(double.PositiveInfinity, "value"));
_ = Assert.ThrowsExactly<ArgumentException>(() => Vector0Guards.EnsureNonNegative(double.NegativeInfinity, "value"));
}
}
Loading