diff --git a/CLAUDE.md b/CLAUDE.md index 1634540..1dd806c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,7 +51,7 @@ These are now baked into the generator and enforced by tests. **Do not reopen wi 1. **`V0 - V0` returns the same `V0` of `T.Abs(a - b)`.** Magnitude subtraction stays non-negative; signed subtraction must use the V1 form explicitly. 2. **Dimensionless and angular quantities have both `Ratio` (V0) and `SignedRatio` (V1) bases.** Ratios that semantically must be non-negative (e.g. `RefractiveIndex`, `MachNumber`, `SpecificGravity`) are V0 overloads of `Ratio`. 3. **Semantic overloads widen implicitly to their base, narrow explicitly from it.** A `Weight` is implicitly a `ForceMagnitude`; the reverse requires `Weight.From(forceMagnitude)` or an explicit cast. -4. **Physical constraints are enforced structurally via the V0 (magnitude) form.** `Vector0` factories run `Vector0Guards.EnsureNonNegative` and throw `ArgumentException` on a negative value. That covers absolute zero (Temperature is V0, so Kelvin must be ≥ 0), non-negative frequency, non-negative absolute pressure, etc. Strict-positive or upper-bound constraints are not yet declared in metadata (tracked separately). +4. **Physical constraints are enforced structurally via the V0 (magnitude) form.** `Vector0` factories run `Vector0Guards.EnsureNonNegative` and throw `ArgumentException` on a negative value. That covers absolute zero (Temperature is V0, so Kelvin must be ≥ 0), non-negative frequency, non-negative absolute pressure, etc. A V0 *overload* can opt into a stricter rule by declaring `physicalConstraints: { "minExclusive": "0" }` in `dimensions.json` (#51); the generator then emits `Vector0Guards.EnsurePositive` and rejects zero too. Used today for `Wavelength`, `Period`, and `HalfLife` — quantities for which zero is unphysical. ### Physical constants diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/HalfLife.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/HalfLife.g.cs index 508f00c..9b41aa3 100644 --- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/HalfLife.g.cs +++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/HalfLife.g.cs @@ -24,49 +24,49 @@ public record HalfLife : PhysicalQuantity, T>, IVector0The value in Second. /// A new HalfLife instance. /// Thrown when the resulting magnitude would be negative. - public static HalfLife FromSeconds(T value) => Create(Vector0Guards.EnsureNonNegative(value, nameof(value))); + public static HalfLife FromSeconds(T value) => Create(Vector0Guards.EnsurePositive(value, nameof(value))); /// /// Creates a new HalfLife from a value in Millisecond. /// /// The value in Millisecond. /// A new HalfLife instance. /// Thrown when the resulting magnitude would be negative. - public static HalfLife FromMilliseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value))); + public static HalfLife FromMilliseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value))); /// /// Creates a new HalfLife from a value in Microsecond. /// /// The value in Microsecond. /// A new HalfLife instance. /// Thrown when the resulting magnitude would be negative. - public static HalfLife FromMicroseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value))); + public static HalfLife FromMicroseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value))); /// /// Creates a new HalfLife from a value in Minute. /// /// The value in Minute. /// A new HalfLife instance. /// Thrown when the resulting magnitude would be negative. - public static HalfLife FromMinutes(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value))); + public static HalfLife FromMinutes(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value))); /// /// Creates a new HalfLife from a value in Hour. /// /// The value in Hour. /// A new HalfLife instance. /// Thrown when the resulting magnitude would be negative. - public static HalfLife FromHours(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value))); + public static HalfLife FromHours(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value))); /// /// Creates a new HalfLife from a value in Day. /// /// The value in Day. /// A new HalfLife instance. /// Thrown when the resulting magnitude would be negative. - public static HalfLife FromDays(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value))); + public static HalfLife FromDays(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value))); /// /// Creates a new HalfLife from a value in Year. /// /// The value in Year. /// A new HalfLife instance. /// Thrown when the resulting magnitude would be negative. - public static HalfLife FromYears(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value))); + public static HalfLife FromYears(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value))); /// Implicit conversion to Duration. public static implicit operator Duration(HalfLife value) => Duration.Create(value.Value); /// Explicit conversion from Duration. diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Period.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Period.g.cs index 601bf0e..bf4b7fd 100644 --- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Period.g.cs +++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Period.g.cs @@ -24,49 +24,49 @@ public record Period : PhysicalQuantity, T>, IVector0, T> /// The value in Second. /// A new Period instance. /// Thrown when the resulting magnitude would be negative. - public static Period FromSeconds(T value) => Create(Vector0Guards.EnsureNonNegative(value, nameof(value))); + public static Period FromSeconds(T value) => Create(Vector0Guards.EnsurePositive(value, nameof(value))); /// /// Creates a new Period from a value in Millisecond. /// /// The value in Millisecond. /// A new Period instance. /// Thrown when the resulting magnitude would be negative. - public static Period FromMilliseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value))); + public static Period FromMilliseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value))); /// /// Creates a new Period from a value in Microsecond. /// /// The value in Microsecond. /// A new Period instance. /// Thrown when the resulting magnitude would be negative. - public static Period FromMicroseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value))); + public static Period FromMicroseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value))); /// /// Creates a new Period from a value in Minute. /// /// The value in Minute. /// A new Period instance. /// Thrown when the resulting magnitude would be negative. - public static Period FromMinutes(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value))); + public static Period FromMinutes(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value))); /// /// Creates a new Period from a value in Hour. /// /// The value in Hour. /// A new Period instance. /// Thrown when the resulting magnitude would be negative. - public static Period FromHours(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value))); + public static Period FromHours(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value))); /// /// Creates a new Period from a value in Day. /// /// The value in Day. /// A new Period instance. /// Thrown when the resulting magnitude would be negative. - public static Period FromDays(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value))); + public static Period FromDays(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value))); /// /// Creates a new Period from a value in Year. /// /// The value in Year. /// A new Period instance. /// Thrown when the resulting magnitude would be negative. - public static Period FromYears(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value))); + public static Period FromYears(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value))); /// Implicit conversion to Duration. public static implicit operator Duration(Period value) => Duration.Create(value.Value); /// Explicit conversion from Duration. diff --git a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Wavelength.g.cs b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Wavelength.g.cs index b2131e4..c321bd2 100644 --- a/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Wavelength.g.cs +++ b/Semantics.Quantities/Generated/Semantics.SourceGenerators/Semantics.SourceGenerators.QuantitiesGenerator/Wavelength.g.cs @@ -24,77 +24,77 @@ public record Wavelength : PhysicalQuantity, T>, IVector0The value in Meter. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromMeters(T value) => Create(Vector0Guards.EnsureNonNegative(value, nameof(value))); + public static Wavelength FromMeters(T value) => Create(Vector0Guards.EnsurePositive(value, nameof(value))); /// /// Creates a new Wavelength from a value in Kilometer. /// /// The value in Kilometer. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromKilometers(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Kilo)), nameof(value))); + public static Wavelength FromKilometers(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Kilo)), nameof(value))); /// /// Creates a new Wavelength from a value in Centimeter. /// /// The value in Centimeter. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromCentimeters(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Centi)), nameof(value))); + public static Wavelength FromCentimeters(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Centi)), nameof(value))); /// /// Creates a new Wavelength from a value in Millimeter. /// /// The value in Millimeter. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromMillimeters(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value))); + public static Wavelength FromMillimeters(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value))); /// /// Creates a new Wavelength from a value in Micrometer. /// /// The value in Micrometer. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromMicrometers(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value))); + public static Wavelength FromMicrometers(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value))); /// /// Creates a new Wavelength from a value in Nanometer. /// /// The value in Nanometer. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromNanometers(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Nano)), nameof(value))); + public static Wavelength FromNanometers(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Nano)), nameof(value))); /// /// Creates a new Wavelength from a value in Angstrom. /// /// The value in Angstrom. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromAngstroms(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.AngstromToMeters)), nameof(value))); + public static Wavelength FromAngstroms(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.AngstromToMeters)), nameof(value))); /// /// Creates a new Wavelength from a value in Foot. /// /// The value in Foot. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromFeet(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.FeetToMeters)), nameof(value))); + public static Wavelength FromFeet(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.FeetToMeters)), nameof(value))); /// /// Creates a new Wavelength from a value in Inch. /// /// The value in Inch. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromInches(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.InchesToMeters)), nameof(value))); + public static Wavelength FromInches(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.InchesToMeters)), nameof(value))); /// /// Creates a new Wavelength from a value in Yard. /// /// The value in Yard. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromYards(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.YardToMeters)), nameof(value))); + public static Wavelength FromYards(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.YardToMeters)), nameof(value))); /// /// Creates a new Wavelength from a value in Mile. /// /// The value in Mile. /// A new Wavelength instance. /// Thrown when the resulting magnitude would be negative. - public static Wavelength FromMiles(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.MileToMeters)), nameof(value))); + public static Wavelength FromMiles(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.MileToMeters)), nameof(value))); /// Implicit conversion to Length. public static implicit operator Length(Wavelength value) => Length.Create(value.Value); /// Explicit conversion from Length. diff --git a/Semantics.Quantities/Vector0Guards.cs b/Semantics.Quantities/Vector0Guards.cs index 0f4c0e8..eab9e2b 100644 --- a/Semantics.Quantities/Vector0Guards.cs +++ b/Semantics.Quantities/Vector0Guards.cs @@ -42,4 +42,30 @@ public static T EnsureNonNegative(T value, string paramName) return value; } + + /// + /// Returns unchanged when it is strictly positive; throws + /// otherwise. Used in generated From{Unit} + /// factories on V0 overloads that declare physicalConstraints.minExclusive: "0" + /// in dimensions.json (per #51). Examples: Wavelength, Period, + /// HalfLife — quantities for which zero is unphysical, distinct from the V0 + /// default that allows zero. + /// + /// The numeric storage type. + /// 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. + public static T EnsurePositive(T value, string paramName) + where T : struct, INumber + { + if (T.Sign(value) <= 0) + { + throw new ArgumentException( + $"Value must be strictly positive; received {value}.", + paramName); + } + + return value; + } } diff --git a/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs b/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs index cd6b916..59866c4 100644 --- a/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs +++ b/Semantics.SourceGenerators/Generators/QuantitiesGenerator.cs @@ -558,6 +558,10 @@ private static Dictionary BuildUnitMap(UnitsMetadata uni /// negative after a unit conversion (e.g. FromCelsius(-300)) — throws /// . This locks in the V0 non-negativity invariant /// from #50 across every per-unit factory introduced for #48. + /// When is also true (V0 overloads with + /// physicalConstraints.minExclusive: "0" per #51) the guard is upgraded to + /// Vector0Guards.EnsurePositive, which rejects zero as well as negative values. + /// is ignored when is false. /// private static void AddUnitFactories( ClassTemplate cls, @@ -566,13 +570,16 @@ private static void AddUnitFactories( string typeName, string fullType, string crefForComment, - bool applyV0Guard) + bool applyV0Guard, + bool strictPositive = false) { if (availableUnits == null || availableUnits.Count == 0) { return; } + string guardMethod = strictPositive ? "EnsurePositive" : "EnsureNonNegative"; + string baseUnit = availableUnits[0]; foreach (string unitName in availableUnits) { @@ -582,7 +589,7 @@ private static void AddUnitFactories( : BuildToBaseExpression(unitName, unitMap); string body = applyV0Guard - ? $" => Create(Vector0Guards.EnsureNonNegative({conversionExpr}, nameof(value)));" + ? $" => Create(Vector0Guards.{guardMethod}({conversionExpr}, nameof(value)));" : $" => Create({conversionExpr});"; // Issue #49: factory names use the plural form. Prefer an explicit FactoryName from @@ -997,7 +1004,12 @@ private void EmitOverloadType( // Factory methods for every available unit (#48); overloads inherit the dimension's // units. V0 overloads enforce the same non-negativity invariant as their V0 base - // type (#50); V1 overloads accept any sign. + // type (#50). V0 overloads that declare physicalConstraints.minExclusive in + // dimensions.json (#51, e.g. Wavelength, Period, HalfLife) get the stricter + // EnsurePositive guard so a zero input is rejected too. V1 overloads accept + // any sign. + bool strictPositive = vectorForm == 0 + && overload.PhysicalConstraints?.MinExclusive == "0"; AddUnitFactories( cls, dim.AvailableUnits, @@ -1005,7 +1017,8 @@ private void EmitOverloadType( typeName, fullType, typeName, - applyV0Guard: vectorForm == 0); + applyV0Guard: vectorForm == 0, + strictPositive: strictPositive); // Implicit widening to base type cls.Members.Add(new MethodTemplate() diff --git a/Semantics.SourceGenerators/Metadata/dimensions.json b/Semantics.SourceGenerators/Metadata/dimensions.json index c1a6bdb..8370346 100644 --- a/Semantics.SourceGenerators/Metadata/dimensions.json +++ b/Semantics.SourceGenerators/Metadata/dimensions.json @@ -46,7 +46,7 @@ }, { "name": "Distance", "description": "Separation between two points in space." }, { "name": "Altitude", "description": "Height above a reference level." }, - { "name": "Wavelength", "description": "Spatial period of a periodic wave." }, + { "name": "Wavelength", "description": "Spatial period of a periodic wave.", "physicalConstraints": { "minExclusive": "0" } }, { "name": "Thickness", "description": "Extent through the thinnest dimension." }, { "name": "Perimeter", "description": "Total boundary length of a 2D shape." } ] @@ -107,8 +107,8 @@ "vector0": { "base": "Duration", "overloads": [ - { "name": "Period", "description": "Time interval for one complete cycle." }, - { "name": "HalfLife", "description": "Time for half of a substance to decay." }, + { "name": "Period", "description": "Time interval for one complete cycle.", "physicalConstraints": { "minExclusive": "0" } }, + { "name": "HalfLife", "description": "Time for half of a substance to decay.", "physicalConstraints": { "minExclusive": "0" } }, { "name": "TimeConstant", "description": "Characteristic response time of a system." }, { "name": "Latency", "description": "Delay before a response begins." }, { "name": "ReverberationTime", "description": "Time for sound to decay by 60 dB in an enclosed space." }, diff --git a/Semantics.SourceGenerators/Models/DimensionsMetadata.cs b/Semantics.SourceGenerators/Models/DimensionsMetadata.cs index 6102f42..d83a9df 100644 --- a/Semantics.SourceGenerators/Models/DimensionsMetadata.cs +++ b/Semantics.SourceGenerators/Models/DimensionsMetadata.cs @@ -170,6 +170,30 @@ public class OverloadDefinition public string Name { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public Dictionary Relationships { get; set; } = []; + + /// + /// Optional per-overload physical constraints. Currently only minExclusive is + /// honoured (per #51): when set on a V0 overload, the generated From{Unit} + /// factories use Vector0Guards.EnsurePositive instead of the default + /// EnsureNonNegative, so a strict-positive overload like Wavelength + /// rejects a zero input. The field is a string so it can hold a literal SI-base-unit + /// value (e.g. "0"); the generator emits T.CreateChecked(...) around it. + /// + public PhysicalConstraints? PhysicalConstraints { get; set; } +} + +/// +/// Per-overload physical constraints applied at construction time, on top of the structural +/// V0 non-negativity invariant. Only MinExclusive is implemented today; the other +/// fields are reserved for future per-overload bounds (#51). +/// +public class PhysicalConstraints +{ + /// + /// Strict-positive lower bound. When set (typically to "0"), the value must be + /// strictly greater than this number after conversion to the SI base unit. + /// + public string MinExclusive { get; set; } = string.Empty; } /// diff --git a/Semantics.Test/Quantities/Vector0InvariantTests.cs b/Semantics.Test/Quantities/Vector0InvariantTests.cs index d053a41..430765d 100644 --- a/Semantics.Test/Quantities/Vector0InvariantTests.cs +++ b/Semantics.Test/Quantities/Vector0InvariantTests.cs @@ -185,4 +185,102 @@ public void Vector0Guards_Throws_On_Negative_With_ParamName() () => Vector0Guards.EnsureNonNegative(-1.0, "myParam")); Assert.AreEqual("myParam", ex.ParamName); } + + // =========================================================== #51: Strict-positive overloads + + // Wavelength, Period, HalfLife declare physicalConstraints.minExclusive: "0" in + // dimensions.json; their generated From{Unit} factories use Vector0Guards.EnsurePositive + // instead of EnsureNonNegative, rejecting zero as well as negative values. + + [TestMethod] + public void Wavelength_FromMeters_Zero_Throws() + => _ = Assert.ThrowsExactly(() => Wavelength.FromMeters(0.0)); + + [TestMethod] + public void Wavelength_FromMeters_Negative_Throws() + => _ = Assert.ThrowsExactly(() => Wavelength.FromMeters(-1e-9)); + + [TestMethod] + public void Wavelength_FromMeters_Positive_Succeeds() + { + Wavelength w = Wavelength.FromMeters(550e-9); + Assert.AreEqual(550e-9, w.Value, 1e-15); + } + + [TestMethod] + public void Wavelength_FromNanometers_Zero_Throws() + => _ = Assert.ThrowsExactly(() => Wavelength.FromNanometers(0.0)); + + [TestMethod] + public void Period_FromSeconds_Zero_Throws() + => _ = Assert.ThrowsExactly(() => Period.FromSeconds(0.0)); + + [TestMethod] + public void Period_FromSeconds_Positive_Succeeds() + { + Period p = Period.FromSeconds(0.001); + Assert.AreEqual(0.001, p.Value, Tolerance); + } + + [TestMethod] + public void HalfLife_FromSeconds_Zero_Throws() + => _ = Assert.ThrowsExactly(() => HalfLife.FromSeconds(0.0)); + + // The base type (Length, Duration) has no minExclusive, so zero is still allowed — + // only the constrained overloads reject it. + + [TestMethod] + public void Length_FromMeters_Zero_Allowed() + { + Length l = Length.FromMeters(0.0); + Assert.AreEqual(0.0, l.Value, Tolerance); + } + + [TestMethod] + public void Duration_FromSeconds_Zero_Allowed() + { + Duration d = Duration.FromSeconds(0.0); + Assert.AreEqual(0.0, d.Value, Tolerance); + } + + // Other Length / Duration overloads without the constraint also still allow zero. + + [TestMethod] + public void Distance_FromMeters_Zero_Allowed() + { + Distance d = Distance.FromMeters(0.0); + Assert.AreEqual(0.0, d.Value, Tolerance); + } + + [TestMethod] + public void Latency_FromSeconds_Zero_Allowed() + { + // Latency has no minExclusive — a zero-latency response is meaningful. + Latency l = Latency.FromSeconds(0.0); + Assert.AreEqual(0.0, l.Value, Tolerance); + } + + // Vector0Guards.EnsurePositive directly. + + [TestMethod] + public void Vector0Guards_EnsurePositive_Allows_Positive() + { + Assert.AreEqual(3.5, Vector0Guards.EnsurePositive(3.5, "v")); + } + + [TestMethod] + public void Vector0Guards_EnsurePositive_Throws_On_Zero_With_ParamName() + { + ArgumentException ex = Assert.ThrowsExactly( + () => Vector0Guards.EnsurePositive(0.0, "myParam")); + Assert.AreEqual("myParam", ex.ParamName); + } + + [TestMethod] + public void Vector0Guards_EnsurePositive_Throws_On_Negative_With_ParamName() + { + ArgumentException ex = Assert.ThrowsExactly( + () => Vector0Guards.EnsurePositive(-1.0, "myParam")); + Assert.AreEqual("myParam", ex.ParamName); + } }