diff --git a/src/DimensionalQuantities.Tests/TypeSystem/QuantityTests.cs b/src/DimensionalQuantities.Tests/TypeSystem/QuantityTests.cs index 06ded5e..e27c681 100644 --- a/src/DimensionalQuantities.Tests/TypeSystem/QuantityTests.cs +++ b/src/DimensionalQuantities.Tests/TypeSystem/QuantityTests.cs @@ -44,6 +44,119 @@ namespace DimensionalQuantities.Tests.TypeSystem; public sealed class QuantityTests { + [Theory] + [InlineData(2, 3, true, false, true, false)] + [InlineData(3, 2, false, true, false, true)] + [InlineData(-2, -2, false, false, true, true)] + [InlineData(double.PositiveInfinity, 3, false, true, false, true)] + public void RelationalOperatorsCompareMatchingDimensions(double leftValue, double rightValue, bool less, bool greater, bool lessOrEqual, bool greaterOrEqual) + { + Current left = Current.FromSiBaseUnit(leftValue); + Current right = Current.FromSiBaseUnit(rightValue); + + Assert.Multiple( + () => Assert.Equal(less, left < right), + () => Assert.Equal(greater, left > right), + () => Assert.Equal(lessOrEqual, left <= right), + () => Assert.Equal(greaterOrEqual, left >= right)); + } + + [Fact] + public void RelationalOperatorsReturnFalseForNanOnEitherSide() + { + Current nan = Current.FromSiBaseUnit(double.NaN); + Current finite = Current.FromSiBaseUnit(1); + + Assert.Multiple( + () => Assert.False(nan < finite), + () => Assert.False(nan > finite), + () => Assert.False(nan <= finite), + () => Assert.False(nan >= finite), + () => Assert.False(finite < nan), + () => Assert.False(finite > nan), + () => Assert.False(finite <= nan), + () => Assert.False(finite >= nan)); + } + + [Theory] + [InlineData(-3, 3)] + [InlineData(3, 3)] + [InlineData(double.NegativeInfinity, double.PositiveInfinity)] + public void AbsReturnsQuantityWithAbsoluteSiMagnitude(double value, double expectedValue) + { + Current original = Current.FromSiBaseUnit(value); + Current result = Current.Abs(original); + Current expected = Current.FromSiBaseUnit(expectedValue); + + Assert.Equal(expected, result); + } + + [Theory] + [InlineData(2, 3, 2, 3)] + [InlineData(3, 2, 2, 3)] + [InlineData(-2, -2, -2, -2)] + public void MinAndMaxReturnMatchingDimension(double leftValue, double rightValue, double expectedMinValue, double expectedMaxValue) + { + Current left = Current.FromSiBaseUnit(leftValue); + Current right = Current.FromSiBaseUnit(rightValue); + Current minResult = Current.Min(left, right); + Current maxResult = Current.Max(left, right); + Current expectedMin = Current.FromSiBaseUnit(expectedMinValue); + Current expectedMax = Current.FromSiBaseUnit(expectedMaxValue); + + Assert.Multiple( + () => Assert.Equal(expectedMin, minResult), + () => Assert.Equal(expectedMax, maxResult)); + } + + [Theory] + [InlineData(-2, 0, 10, 0)] + [InlineData(5, 0, 10, 5)] + [InlineData(12, 0, 10, 10)] + public void ClampReturnsQuantityWithinBounds(double value, double minValue, double maxValue, double expectedValue) + { + Current original = Current.FromSiBaseUnit(value); + Current min = Current.FromSiBaseUnit(minValue); + Current max = Current.FromSiBaseUnit(maxValue); + Current result = Current.Clamp(original, min, max); + Current expected = Current.FromSiBaseUnit(expectedValue); + + Assert.Equal(expected, result); + } + + [Fact] + public void ClampRejectsReversedBounds() + { + Current original = Current.FromSiBaseUnit(5); + Current min = Current.FromSiBaseUnit(10); + Current max = Current.FromSiBaseUnit(0); + + Assert.Throws(() => Current.Clamp(original, min, max)); + } + + [Fact] + public void HelpersFollowDoubleNanAndSignedZeroBehavior() + { + Current nan = Current.FromSiBaseUnit(double.NaN); + Current finite = Current.FromSiBaseUnit(1); + Current negativeZero = Current.FromSiBaseUnit(-0.0); + Current positiveZero = Current.FromSiBaseUnit(0.0); + Current absResult = Current.Abs(negativeZero); + Current clampResult = Current.Clamp(negativeZero, negativeZero, positiveZero); + Current nanMinimumResult = Current.Clamp(Current.FromSiBaseUnit(2), nan, finite); + Current nanMaximumResult = Current.Clamp(Current.FromSiBaseUnit(-1), positiveZero, nan); + + Assert.Multiple( + () => Assert.True(double.IsNaN(Current.Abs(nan).AsSiBaseUnit())), + () => Assert.True(double.IsNaN(Current.Min(nan, finite).AsSiBaseUnit())), + () => Assert.True(double.IsNaN(Current.Max(finite, nan).AsSiBaseUnit())), + () => Assert.True(double.IsNaN(Current.Clamp(nan, negativeZero, finite).AsSiBaseUnit())), + () => Assert.Equal(finite, nanMinimumResult), + () => Assert.Equal(positiveZero, nanMaximumResult), + () => Assert.Equal(0L, BitConverter.DoubleToInt64Bits(absResult.AsSiBaseUnit())), + () => Assert.Equal(long.MinValue, BitConverter.DoubleToInt64Bits(clampResult.AsSiBaseUnit()))); + } + [Theory] [InlineData(6, 2, 3)] [InlineData(-6, 2, -3)] diff --git a/src/DimensionalQuantities/TypeSystem/Quantity.cs b/src/DimensionalQuantities/TypeSystem/Quantity.cs index 058b867..fde1c2e 100644 --- a/src/DimensionalQuantities/TypeSystem/Quantity.cs +++ b/src/DimensionalQuantities/TypeSystem/Quantity.cs @@ -9,18 +9,17 @@ namespace DimensionalQuantities.TypeSystem; [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] public readonly record struct Quantity { - public static Quantity operator +(Quantity left, Quantity right) => new(left.ValueInSiBaseUnit + right.ValueInSiBaseUnit); - public static Quantity operator -(Quantity left, Quantity right) => new(left.ValueInSiBaseUnit - right.ValueInSiBaseUnit); - public static Quantity operator -(Quantity quantity) => new(-quantity.ValueInSiBaseUnit); - public static Quantity operator *(Quantity quantity, double scalar) => new(quantity.ValueInSiBaseUnit * scalar); - public static Quantity operator *(double scalar, Quantity quantity) => new(scalar * quantity.ValueInSiBaseUnit); - public static Quantity operator /(Quantity quantity, double scalar) => new(quantity.ValueInSiBaseUnit / scalar); - public static double operator /(Quantity left, Quantity right) => left.ValueInSiBaseUnit / right.ValueInSiBaseUnit; - // Each closed quantity type intentionally caches its own decoded dimension. // ReSharper disable once StaticMemberInGenericType private static readonly Lazy _dimension = new(ParseDimension); + internal Quantity(double valueInSiBaseUnit) + { + ValueInSiBaseUnit = valueInSiBaseUnit; + } + + internal double ValueInSiBaseUnit { get; } + /// Creates a quantity from a magnitude expressed in coherent SI base units for its dimension. public static Quantity FromSiBaseUnit(double value) => new(value); @@ -41,13 +40,6 @@ public static Quantity FromArbitrarySiComponents( return new(value * Math.Pow(10, exponent)); } - internal Quantity(double valueInSiBaseUnit) - { - ValueInSiBaseUnit = valueInSiBaseUnit; - } - - internal double ValueInSiBaseUnit { get; } - /// Returns the magnitude in coherent SI base units for its dimension. public double AsSiBaseUnit() => ValueInSiBaseUnit; @@ -67,6 +59,56 @@ public double AsArbitrarySiComponents( return ValueInSiBaseUnit / Math.Pow(10, exponent); } + public static Quantity operator +(Quantity left, Quantity right) => new(left.ValueInSiBaseUnit + right.ValueInSiBaseUnit); + public static Quantity operator -(Quantity left, Quantity right) => new(left.ValueInSiBaseUnit - right.ValueInSiBaseUnit); + public static Quantity operator -(Quantity quantity) => new(-quantity.ValueInSiBaseUnit); + public static Quantity operator *(Quantity quantity, double scalar) => new(quantity.ValueInSiBaseUnit * scalar); + public static Quantity operator *(double scalar, Quantity quantity) => new(scalar * quantity.ValueInSiBaseUnit); + public static Quantity operator /(Quantity quantity, double scalar) => new(quantity.ValueInSiBaseUnit / scalar); + public static double operator /(Quantity left, Quantity right) => left.ValueInSiBaseUnit / right.ValueInSiBaseUnit; + + /// Compares two quantities of the same dimension by their SI magnitudes. + public static bool operator <(Quantity left, Quantity right) => left.ValueInSiBaseUnit < right.ValueInSiBaseUnit; + + /// Compares two quantities of the same dimension by their SI magnitudes. + public static bool operator >(Quantity left, Quantity right) => left.ValueInSiBaseUnit > right.ValueInSiBaseUnit; + + /// Compares two quantities of the same dimension by their SI magnitudes. + public static bool operator <=(Quantity left, Quantity right) => left.ValueInSiBaseUnit <= right.ValueInSiBaseUnit; + + /// Compares two quantities of the same dimension by their SI magnitudes. + public static bool operator >=(Quantity left, Quantity right) => left.ValueInSiBaseUnit >= right.ValueInSiBaseUnit; + + /// Returns the absolute magnitude of a quantity. + public static Quantity Abs(Quantity quantity) => new(Math.Abs(quantity.ValueInSiBaseUnit)); + + /// Returns the smaller of two quantities of the same dimension. + public static Quantity Min(Quantity left, Quantity right) => new(Math.Min(left.ValueInSiBaseUnit, right.ValueInSiBaseUnit)); + + /// Returns the larger of two quantities of the same dimension. + public static Quantity Max(Quantity left, Quantity right) => new(Math.Max(left.ValueInSiBaseUnit, right.ValueInSiBaseUnit)); + + /// Restricts a quantity to the inclusive range between two quantities of the same dimension. + public static Quantity Clamp(Quantity value, Quantity min, Quantity max) + { + if (min > max) + { + throw new ArgumentException("The minimum must be less than or equal to the maximum.", nameof(min)); + } + + if (value < min) + { + return min; + } + + if (value > max) + { + return max; + } + + return value; + } + private static double GetScaleExponent(int length, int mass, int time, int electricCurrent, int temperature, int amountOfSubstance, int luminousIntensity, double overall) { MathematicalDimension dimension = _dimension.Value;