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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,49 @@ public record HalfLife<T> : PhysicalQuantity<HalfLife<T>, T>, IVector0<HalfLife<
/// <param name="value">The value in Second.</param>
/// <returns>A new HalfLife instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static HalfLife<T> FromSeconds(T value) => Create(Vector0Guards.EnsureNonNegative(value, nameof(value)));
public static HalfLife<T> FromSeconds(T value) => Create(Vector0Guards.EnsurePositive(value, nameof(value)));
/// <summary>
/// Creates a new HalfLife from a value in Millisecond.
/// </summary>
/// <param name="value">The value in Millisecond.</param>
/// <returns>A new HalfLife instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static HalfLife<T> FromMilliseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value)));
public static HalfLife<T> FromMilliseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value)));
/// <summary>
/// Creates a new HalfLife from a value in Microsecond.
/// </summary>
/// <param name="value">The value in Microsecond.</param>
/// <returns>A new HalfLife instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static HalfLife<T> FromMicroseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value)));
public static HalfLife<T> FromMicroseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value)));
/// <summary>
/// Creates a new HalfLife from a value in Minute.
/// </summary>
/// <param name="value">The value in Minute.</param>
/// <returns>A new HalfLife instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static HalfLife<T> FromMinutes(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value)));
public static HalfLife<T> FromMinutes(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value)));
/// <summary>
/// Creates a new HalfLife from a value in Hour.
/// </summary>
/// <param name="value">The value in Hour.</param>
/// <returns>A new HalfLife instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static HalfLife<T> FromHours(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value)));
public static HalfLife<T> FromHours(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value)));
/// <summary>
/// Creates a new HalfLife from a value in Day.
/// </summary>
/// <param name="value">The value in Day.</param>
/// <returns>A new HalfLife instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static HalfLife<T> FromDays(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value)));
public static HalfLife<T> FromDays(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value)));
/// <summary>
/// Creates a new HalfLife from a value in Year.
/// </summary>
/// <param name="value">The value in Year.</param>
/// <returns>A new HalfLife instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static HalfLife<T> FromYears(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value)));
public static HalfLife<T> FromYears(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value)));
/// <summary>Implicit conversion to Duration.</summary>
public static implicit operator Duration<T>(HalfLife<T> value) => Duration<T>.Create(value.Value);
/// <summary>Explicit conversion from Duration.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,49 +24,49 @@ public record Period<T> : PhysicalQuantity<Period<T>, T>, IVector0<Period<T>, T>
/// <param name="value">The value in Second.</param>
/// <returns>A new Period instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Period<T> FromSeconds(T value) => Create(Vector0Guards.EnsureNonNegative(value, nameof(value)));
public static Period<T> FromSeconds(T value) => Create(Vector0Guards.EnsurePositive(value, nameof(value)));
/// <summary>
/// Creates a new Period from a value in Millisecond.
/// </summary>
/// <param name="value">The value in Millisecond.</param>
/// <returns>A new Period instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Period<T> FromMilliseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value)));
public static Period<T> FromMilliseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value)));
/// <summary>
/// Creates a new Period from a value in Microsecond.
/// </summary>
/// <param name="value">The value in Microsecond.</param>
/// <returns>A new Period instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Period<T> FromMicroseconds(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value)));
public static Period<T> FromMicroseconds(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value)));
/// <summary>
/// Creates a new Period from a value in Minute.
/// </summary>
/// <param name="value">The value in Minute.</param>
/// <returns>A new Period instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Period<T> FromMinutes(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value)));
public static Period<T> FromMinutes(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.MinuteToSeconds)), nameof(value)));
/// <summary>
/// Creates a new Period from a value in Hour.
/// </summary>
/// <param name="value">The value in Hour.</param>
/// <returns>A new Period instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Period<T> FromHours(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value)));
public static Period<T> FromHours(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.HourToSeconds)), nameof(value)));
/// <summary>
/// Creates a new Period from a value in Day.
/// </summary>
/// <param name="value">The value in Day.</param>
/// <returns>A new Period instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Period<T> FromDays(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value)));
public static Period<T> FromDays(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.DayToSeconds)), nameof(value)));
/// <summary>
/// Creates a new Period from a value in Year.
/// </summary>
/// <param name="value">The value in Year.</param>
/// <returns>A new Period instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Period<T> FromYears(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value)));
public static Period<T> FromYears(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.YearToSeconds)), nameof(value)));
/// <summary>Implicit conversion to Duration.</summary>
public static implicit operator Duration<T>(Period<T> value) => Duration<T>.Create(value.Value);
/// <summary>Explicit conversion from Duration.</summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,77 +24,77 @@ public record Wavelength<T> : PhysicalQuantity<Wavelength<T>, T>, IVector0<Wavel
/// <param name="value">The value in Meter.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromMeters(T value) => Create(Vector0Guards.EnsureNonNegative(value, nameof(value)));
public static Wavelength<T> FromMeters(T value) => Create(Vector0Guards.EnsurePositive(value, nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Kilometer.
/// </summary>
/// <param name="value">The value in Kilometer.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromKilometers(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Kilo)), nameof(value)));
public static Wavelength<T> FromKilometers(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Kilo)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Centimeter.
/// </summary>
/// <param name="value">The value in Centimeter.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromCentimeters(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Centi)), nameof(value)));
public static Wavelength<T> FromCentimeters(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Centi)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Millimeter.
/// </summary>
/// <param name="value">The value in Millimeter.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromMillimeters(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value)));
public static Wavelength<T> FromMillimeters(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Milli)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Micrometer.
/// </summary>
/// <param name="value">The value in Micrometer.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromMicrometers(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value)));
public static Wavelength<T> FromMicrometers(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Micro)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Nanometer.
/// </summary>
/// <param name="value">The value in Nanometer.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromNanometers(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(MetricMagnitudes.Nano)), nameof(value)));
public static Wavelength<T> FromNanometers(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(MetricMagnitudes.Nano)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Angstrom.
/// </summary>
/// <param name="value">The value in Angstrom.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromAngstroms(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.AngstromToMeters)), nameof(value)));
public static Wavelength<T> FromAngstroms(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.AngstromToMeters)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Foot.
/// </summary>
/// <param name="value">The value in Foot.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromFeet(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.FeetToMeters)), nameof(value)));
public static Wavelength<T> FromFeet(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.FeetToMeters)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Inch.
/// </summary>
/// <param name="value">The value in Inch.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromInches(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.InchesToMeters)), nameof(value)));
public static Wavelength<T> FromInches(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.InchesToMeters)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Yard.
/// </summary>
/// <param name="value">The value in Yard.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromYards(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.YardToMeters)), nameof(value)));
public static Wavelength<T> FromYards(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.YardToMeters)), nameof(value)));
/// <summary>
/// Creates a new Wavelength from a value in Mile.
/// </summary>
/// <param name="value">The value in Mile.</param>
/// <returns>A new Wavelength instance.</returns>
/// <exception cref="System.ArgumentException">Thrown when the resulting magnitude would be negative.</exception>
public static Wavelength<T> FromMiles(T value) => Create(Vector0Guards.EnsureNonNegative((value * T.CreateChecked(Units.ConversionConstants.MileToMeters)), nameof(value)));
public static Wavelength<T> FromMiles(T value) => Create(Vector0Guards.EnsurePositive((value * T.CreateChecked(Units.ConversionConstants.MileToMeters)), nameof(value)));
/// <summary>Implicit conversion to Length.</summary>
public static implicit operator Length<T>(Wavelength<T> value) => Length<T>.Create(value.Value);
/// <summary>Explicit conversion from Length.</summary>
Expand Down
26 changes: 26 additions & 0 deletions Semantics.Quantities/Vector0Guards.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,30 @@ public static T EnsureNonNegative<T>(T value, string paramName)

return value;
}

/// <summary>
/// Returns <paramref name="value"/> unchanged when it is strictly positive; throws
/// <see cref="ArgumentException"/> otherwise. Used in generated <c>From{Unit}</c>
/// factories on V0 overloads that declare <c>physicalConstraints.minExclusive: "0"</c>
/// in <c>dimensions.json</c> (per #51). Examples: <c>Wavelength</c>, <c>Period</c>,
/// <c>HalfLife</c> — quantities for which zero is unphysical, distinct from the V0
/// default that allows zero.
/// </summary>
/// <typeparam name="T">The numeric storage type.</typeparam>
/// <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>
public static T EnsurePositive<T>(T value, string paramName)
where T : struct, INumber<T>
{
if (T.Sign(value) <= 0)
{
throw new ArgumentException(
$"Value must be strictly positive; received {value}.",
paramName);
}

return value;
}
}
Loading
Loading