Skip to content

Commit

Permalink
Add type safety and improve type inference (#1374)
Browse files Browse the repository at this point in the history
Some of the extension methods in `UnitMath.cs` (e.g. Average) take an
`Enum unitType` argument.
The compiler should be able to detect when the units type doesn't match
the quantity type.

* This will break backwards compatibility, but only for:
  * People doing really weird things.
* People using the explicit generic type instead of inference (e.g.
`Average<Length>(LengthUnit.Inch)`), in which case they can fix the code
by changing it to e.g. `Average<Length, LengthUnit>(LengthUnit.Inch)`
* This change might be warranted in `UnitConverter.cs` as well, but
can't be implemented as a straight-forward refactor since it breaks
compatibility in generated code (e.g.
`unitConverter.SetConversionFunction<ElectricPotential>` in
`ElectricPotential` should be `SetConversionFunction<ElectricPotential,
ElectricPotentialUnit>`

In addition, had to remove a few unit tests that were asserting type
safety.
All tests that were removed:
* Were only testing that an incorrect behavior throws an exception
(`Assert.Throws`)
* Don't compile after the changes in `UnitMath.cs`
  • Loading branch information
UrielZyx authored Mar 3, 2024
1 parent 5572dc2 commit 6a9ae6c
Show file tree
Hide file tree
Showing 2 changed files with 32 additions and 84 deletions.
64 changes: 0 additions & 64 deletions UnitsNet.Tests/UnitMathTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,6 @@ public void AbsoluteValueOfNullReferenceThrowsException()
Assert.Throws<NullReferenceException>(() => quantity.Abs());
}

[Fact]
public void AverageOfDifferentUnitsThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), Volume.FromLiters(50)};

Assert.Throws<ArgumentException>(() => units.Average(LengthUnit.Centimeter));
}

[Fact]
public void AverageOfEmptySourceThrowsException()
{
Expand All @@ -61,14 +53,6 @@ public void AverageOfEmptySourceThrowsException()
Assert.Throws<InvalidOperationException>(() => units.Average(LengthUnit.Centimeter));
}

[Fact]
public void AverageOfLengthsWithNullValueThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), null!};

Assert.Throws<NullReferenceException>(() => units.Average(LengthUnit.Centimeter));
}

[Fact]
public void AverageOfLengthsCalculatesCorrectly()
{
Expand Down Expand Up @@ -119,22 +103,6 @@ public void MaxOfTwoLengthsReturnsTheLargestValue()
Assert.Equal(LengthUnit.Meter, max.Unit);
}

[Fact]
public void MaxOfDifferentUnitsThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), Volume.FromLiters(50)};

Assert.Throws<ArgumentException>(() => units.Max(LengthUnit.Centimeter));
}

[Fact]
public void MaxOfLengthsWithNullValueThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), null!};

Assert.Throws<NullReferenceException>(() => units.Max(LengthUnit.Centimeter));
}

[Fact]
public void MaxOfEmptySourceThrowsException()
{
Expand Down Expand Up @@ -193,22 +161,6 @@ public void MinOfTwoLengthsReturnsTheSmallestValue()
Assert.Equal(LengthUnit.Centimeter, min.Unit);
}

[Fact]
public void MinOfDifferentUnitsThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), Volume.FromLiters(50)};

Assert.Throws<ArgumentException>(() => units.Min(LengthUnit.Centimeter));
}

[Fact]
public void MinOfLengthsWithNullValueThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), null!};

Assert.Throws<NullReferenceException>(() => units.Min(LengthUnit.Centimeter));
}

[Fact]
public void MinOfEmptySourceThrowsException()
{
Expand Down Expand Up @@ -255,22 +207,6 @@ public void MinOfLengthsWithSelectorCalculatesCorrectly()
Assert.Equal(LengthUnit.Centimeter, min.Unit);
}

[Fact]
public void SumOfDifferentUnitsThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), Volume.FromLiters(50)};

Assert.Throws<ArgumentException>(() => units.Sum(LengthUnit.Centimeter));
}

[Fact]
public void SumOfLengthsWithNullValueThrowsException()
{
var units = new IQuantity[] {Length.FromMeters(1), null!};

Assert.Throws<NullReferenceException>(() => units.Sum(LengthUnit.Centimeter));
}

[Fact]
public void SumOfEmptySourceReturnsZero()
{
Expand Down
52 changes: 32 additions & 20 deletions UnitsNet/UnitMath.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ public static TQuantity Abs<TQuantity>(this TQuantity value) where TQuantity : I
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Sum<TQuantity>(this IEnumerable<TQuantity> source, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Sum<TQuantity, TUnitType>(this IEnumerable<TQuantity> source, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return (TQuantity) Quantity.From(source.Sum(x => x.As(unitType)), unitType);
}
Expand All @@ -44,16 +45,18 @@ public static TQuantity Sum<TQuantity>(this IEnumerable<TQuantity> source, Enum
/// <param name="selector">A transform function to apply to each element.</param>
/// <param name="unitType">The desired unit type for the resulting quantity</param>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation.</typeparam>
/// <typeparam name="TUnitType">The type of unit enum.</typeparam>
/// <returns>The sum of the projected values, represented in the specified unit type.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="source">source</paramref> or <paramref name="selector">selector</paramref> is null.
/// </exception>
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Sum<TSource, TQuantity>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Sum<TSource, TQuantity, TUnitType>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return source.Select(selector).Sum(unitType);
}
Expand All @@ -79,8 +82,9 @@ public static TQuantity Min<TQuantity>(TQuantity val1, TQuantity val2) where TQu
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Min<TQuantity>(this IEnumerable<TQuantity> source, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Min<TQuantity, TUnitType>(this IEnumerable<TQuantity> source, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return (TQuantity) Quantity.From(source.Min(x => x.As(unitType)), unitType);
}
Expand All @@ -93,7 +97,8 @@ public static TQuantity Min<TQuantity>(this IEnumerable<TQuantity> source, Enum
/// <param name="selector">A transform function to apply to each element.</param>
/// <param name="unitType">The desired unit type for the resulting quantity</param>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation.</typeparam>
/// <typeparam name="TUnitType">The type of unit enum.</typeparam>
/// <returns>The min of the projected values, represented in the specified unit type.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="source">source</paramref> or <paramref name="selector">selector</paramref> is null.
Expand All @@ -102,8 +107,9 @@ public static TQuantity Min<TQuantity>(this IEnumerable<TQuantity> source, Enum
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Min<TSource, TQuantity>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Min<TSource, TQuantity, TUnitType>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return source.Select(selector).Min(unitType);
}
Expand All @@ -129,8 +135,9 @@ public static TQuantity Max<TQuantity>(TQuantity val1, TQuantity val2) where TQu
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Max<TQuantity>(this IEnumerable<TQuantity> source, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Max<TQuantity, TUnitType>(this IEnumerable<TQuantity> source, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return (TQuantity) Quantity.From(source.Max(x => x.As(unitType)), unitType);
}
Expand All @@ -143,7 +150,8 @@ public static TQuantity Max<TQuantity>(this IEnumerable<TQuantity> source, Enum
/// <param name="selector">A transform function to apply to each element.</param>
/// <param name="unitType">The desired unit type for the resulting quantity</param>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation.</typeparam>
/// <typeparam name="TUnitType">The type of unit enum.</typeparam>
/// <returns>The max of the projected values, represented in the specified unit type.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="source">source</paramref> or <paramref name="selector">selector</paramref> is null.
Expand All @@ -152,8 +160,9 @@ public static TQuantity Max<TQuantity>(this IEnumerable<TQuantity> source, Enum
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Max<TSource, TQuantity>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Max<TSource, TQuantity, TUnitType>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return source.Select(selector).Max(unitType);
}
Expand All @@ -169,8 +178,9 @@ public static TQuantity Max<TSource, TQuantity>(this IEnumerable<TSource> source
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Average<TQuantity>(this IEnumerable<TQuantity> source, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Average<TQuantity, TUnitType>(this IEnumerable<TQuantity> source, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return (TQuantity) Quantity.From(source.Average(x => x.As(unitType)), unitType);
}
Expand All @@ -183,7 +193,8 @@ public static TQuantity Average<TQuantity>(this IEnumerable<TQuantity> source, E
/// <param name="selector">A transform function to apply to each element.</param>
/// <param name="unitType">The desired unit type for the resulting quantity</param>
/// <typeparam name="TSource">The type of the elements of source.</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation</typeparam>
/// <typeparam name="TQuantity">The type of quantity that is produced by this operation.</typeparam>
/// <typeparam name="TUnitType">The type of unit enum.</typeparam>
/// <returns>The average of the projected values, represented in the specified unit type.</returns>
/// <exception cref="T:System.ArgumentNullException">
/// <paramref name="source">source</paramref> or <paramref name="selector">selector</paramref> is null.
Expand All @@ -192,8 +203,9 @@ public static TQuantity Average<TQuantity>(this IEnumerable<TQuantity> source, E
/// <exception cref="ArgumentException">
/// <paramref name="source">source</paramref> contains quantity types different from <paramref name="unitType" />.
/// </exception>
public static TQuantity Average<TSource, TQuantity>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, Enum unitType)
where TQuantity : IQuantity
public static TQuantity Average<TSource, TQuantity, TUnitType>(this IEnumerable<TSource> source, Func<TSource, TQuantity> selector, TUnitType unitType)
where TUnitType : Enum
where TQuantity : IQuantity<TUnitType>
{
return source.Select(selector).Average(unitType);
}
Expand Down

0 comments on commit 6a9ae6c

Please sign in to comment.