From ebcd43427414896f240723bcf43e3c8d031c588b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 13 Oct 2026 04:25:26 +0000 Subject: [PATCH] Reject NaN in Vector0 guards with ArgumentException [patch] T.Sign(NaN) throws ArithmeticException for double, float and Half, so the documented ArgumentException was never reached. Check T.IsNaN first, then compare against zero instead of taking the sign. Fixes #326 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01QbGcYckSzgr6jPyoNYhWvA --- Semantics.Quantities/Vector0Guards.cs | 26 +++++++++++--- .../Quantities/Vector0InvariantTests.cs | 34 +++++++++++++++++++ 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/Semantics.Quantities/Vector0Guards.cs b/Semantics.Quantities/Vector0Guards.cs index 548d64a6..c0fbc8ba 100644 --- a/Semantics.Quantities/Vector0Guards.cs +++ b/Semantics.Quantities/Vector0Guards.cs @@ -27,11 +27,13 @@ public static class Vector0Guards /// The value (already converted to the SI base unit) to validate. /// Name of the originating parameter, used for the exception message. /// The validated, non-negative value. - /// When is negative. + /// When is NaN or negative. public static T EnsureNonNegative(T value, string paramName) where T : struct, INumber { - if (T.Sign(value) < 0) + EnsureNumber(value, paramName); + + if (value < T.Zero) { throw new ArgumentException( $"Magnitude must be non-negative; received {value}.", @@ -53,11 +55,13 @@ public static T EnsureNonNegative(T value, string paramName) /// The value (already converted to the SI base unit) to validate. /// Name of the originating parameter, used for the exception message. /// The validated, strictly-positive value. - /// When is zero or negative. + /// When is NaN, zero or negative. public static T EnsurePositive(T value, string paramName) where T : struct, INumber { - if (T.Sign(value) <= 0) + EnsureNumber(value, paramName); + + if (value <= T.Zero) { throw new ArgumentException( $"Value must be strictly positive; received {value}.", @@ -66,4 +70,18 @@ public static T EnsurePositive(T value, string paramName) return value; } + + /// + /// Throws when is NaN. Checked before the + /// sign test because T.Sign(NaN) throws for the IEEE types, + /// and a NaN compares false against zero, so neither test rejects it on its own. + /// + private static void EnsureNumber(T value, string paramName) + where T : struct, INumber + { + if (T.IsNaN(value)) + { + throw new ArgumentException("Magnitude must be a number; received NaN.", paramName); + } + } } diff --git a/Semantics.Test/Quantities/Vector0InvariantTests.cs b/Semantics.Test/Quantities/Vector0InvariantTests.cs index af58da18..6228feab 100644 --- a/Semantics.Test/Quantities/Vector0InvariantTests.cs +++ b/Semantics.Test/Quantities/Vector0InvariantTests.cs @@ -12,6 +12,7 @@ namespace ktsu.Semantics.Test.Quantities; /// Issue #50: factories reject negative inputs with . /// Issue #52: V0 - V0 returns the same V0 of T.Abs(a - b). /// Issue #285: a V0 has no unary negation, and scaling it by a negative number throws. +/// Issue #326: a NaN input is rejected with , not . /// /// [TestClass] @@ -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.FromMeterPerSecond(2.0) * -3.0).Value, Tolerance); + + // =========================================================== #326: NaN is an ArgumentException + + [TestMethod] + public void EnsureNonNegative_NaN_Throws_ArgumentException() + { + _ = Assert.ThrowsExactly(() => Vector0Guards.EnsureNonNegative(double.NaN, "value")); + _ = Assert.ThrowsExactly(() => Vector0Guards.EnsureNonNegative(float.NaN, "value")); + _ = Assert.ThrowsExactly(() => Vector0Guards.EnsureNonNegative(Half.NaN, "value")); + } + + [TestMethod] + public void EnsurePositive_NaN_Throws_ArgumentException() + { + _ = Assert.ThrowsExactly(() => Vector0Guards.EnsurePositive(double.NaN, "value")); + _ = Assert.ThrowsExactly(() => Vector0Guards.EnsurePositive(float.NaN, "value")); + _ = Assert.ThrowsExactly(() => Vector0Guards.EnsurePositive(Half.NaN, "value")); + } + + [TestMethod] + public void Generated_Factories_NaN_Throw_ArgumentException() + { + _ = Assert.ThrowsExactly(() => Speed.FromMeterPerSecond(double.NaN)); + _ = Assert.ThrowsExactly(() => Mass.FromKilogram(double.NaN)); + _ = Assert.ThrowsExactly(() => Wavelength.FromMeter(double.NaN)); + } + + [TestMethod] + public void Guards_Accept_Infinity_And_Reject_NegativeInfinity() + { + Assert.AreEqual(double.PositiveInfinity, Vector0Guards.EnsurePositive(double.PositiveInfinity, "value")); + _ = Assert.ThrowsExactly(() => Vector0Guards.EnsureNonNegative(double.NegativeInfinity, "value")); + } }