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
113 changes: 113 additions & 0 deletions src/DimensionalQuantities.Tests/TypeSystem/QuantityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ArgumentException>(() => 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)]
Expand Down
72 changes: 57 additions & 15 deletions src/DimensionalQuantities/TypeSystem/Quantity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@ namespace DimensionalQuantities.TypeSystem;
[UsedImplicitly(ImplicitUseTargetFlags.WithMembers)]
public readonly record struct Quantity<TDimension>
{
public static Quantity<TDimension> operator +(Quantity<TDimension> left, Quantity<TDimension> right) => new(left.ValueInSiBaseUnit + right.ValueInSiBaseUnit);
public static Quantity<TDimension> operator -(Quantity<TDimension> left, Quantity<TDimension> right) => new(left.ValueInSiBaseUnit - right.ValueInSiBaseUnit);
public static Quantity<TDimension> operator -(Quantity<TDimension> quantity) => new(-quantity.ValueInSiBaseUnit);
public static Quantity<TDimension> operator *(Quantity<TDimension> quantity, double scalar) => new(quantity.ValueInSiBaseUnit * scalar);
public static Quantity<TDimension> operator *(double scalar, Quantity<TDimension> quantity) => new(scalar * quantity.ValueInSiBaseUnit);
public static Quantity<TDimension> operator /(Quantity<TDimension> quantity, double scalar) => new(quantity.ValueInSiBaseUnit / scalar);
public static double operator /(Quantity<TDimension> left, Quantity<TDimension> right) => left.ValueInSiBaseUnit / right.ValueInSiBaseUnit;

// Each closed quantity type intentionally caches its own decoded dimension.
// ReSharper disable once StaticMemberInGenericType
private static readonly Lazy<MathematicalDimension> _dimension = new(ParseDimension);

internal Quantity(double valueInSiBaseUnit)
{
ValueInSiBaseUnit = valueInSiBaseUnit;
}

internal double ValueInSiBaseUnit { get; }

/// <summary>Creates a quantity from a magnitude expressed in coherent SI base units for its dimension.</summary>
public static Quantity<TDimension> FromSiBaseUnit(double value) => new(value);

Expand All @@ -41,13 +40,6 @@ public static Quantity<TDimension> FromArbitrarySiComponents(
return new(value * Math.Pow(10, exponent));
}

internal Quantity(double valueInSiBaseUnit)
{
ValueInSiBaseUnit = valueInSiBaseUnit;
}

internal double ValueInSiBaseUnit { get; }

/// <summary>Returns the magnitude in coherent SI base units for its dimension.</summary>
public double AsSiBaseUnit() => ValueInSiBaseUnit;

Expand All @@ -67,6 +59,56 @@ public double AsArbitrarySiComponents(
return ValueInSiBaseUnit / Math.Pow(10, exponent);
}

public static Quantity<TDimension> operator +(Quantity<TDimension> left, Quantity<TDimension> right) => new(left.ValueInSiBaseUnit + right.ValueInSiBaseUnit);
public static Quantity<TDimension> operator -(Quantity<TDimension> left, Quantity<TDimension> right) => new(left.ValueInSiBaseUnit - right.ValueInSiBaseUnit);
public static Quantity<TDimension> operator -(Quantity<TDimension> quantity) => new(-quantity.ValueInSiBaseUnit);
public static Quantity<TDimension> operator *(Quantity<TDimension> quantity, double scalar) => new(quantity.ValueInSiBaseUnit * scalar);
public static Quantity<TDimension> operator *(double scalar, Quantity<TDimension> quantity) => new(scalar * quantity.ValueInSiBaseUnit);
public static Quantity<TDimension> operator /(Quantity<TDimension> quantity, double scalar) => new(quantity.ValueInSiBaseUnit / scalar);
public static double operator /(Quantity<TDimension> left, Quantity<TDimension> right) => left.ValueInSiBaseUnit / right.ValueInSiBaseUnit;

/// <summary>Compares two quantities of the same dimension by their SI magnitudes.</summary>
public static bool operator <(Quantity<TDimension> left, Quantity<TDimension> right) => left.ValueInSiBaseUnit < right.ValueInSiBaseUnit;

/// <summary>Compares two quantities of the same dimension by their SI magnitudes.</summary>
public static bool operator >(Quantity<TDimension> left, Quantity<TDimension> right) => left.ValueInSiBaseUnit > right.ValueInSiBaseUnit;

/// <summary>Compares two quantities of the same dimension by their SI magnitudes.</summary>
public static bool operator <=(Quantity<TDimension> left, Quantity<TDimension> right) => left.ValueInSiBaseUnit <= right.ValueInSiBaseUnit;

/// <summary>Compares two quantities of the same dimension by their SI magnitudes.</summary>
public static bool operator >=(Quantity<TDimension> left, Quantity<TDimension> right) => left.ValueInSiBaseUnit >= right.ValueInSiBaseUnit;

/// <summary>Returns the absolute magnitude of a quantity.</summary>
public static Quantity<TDimension> Abs(Quantity<TDimension> quantity) => new(Math.Abs(quantity.ValueInSiBaseUnit));

/// <summary>Returns the smaller of two quantities of the same dimension.</summary>
public static Quantity<TDimension> Min(Quantity<TDimension> left, Quantity<TDimension> right) => new(Math.Min(left.ValueInSiBaseUnit, right.ValueInSiBaseUnit));

/// <summary>Returns the larger of two quantities of the same dimension.</summary>
public static Quantity<TDimension> Max(Quantity<TDimension> left, Quantity<TDimension> right) => new(Math.Max(left.ValueInSiBaseUnit, right.ValueInSiBaseUnit));

/// <summary>Restricts a quantity to the inclusive range between two quantities of the same dimension.</summary>
public static Quantity<TDimension> Clamp(Quantity<TDimension> value, Quantity<TDimension> min, Quantity<TDimension> 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;
Expand Down
Loading