Skip to content

Commit

Permalink
Constrained the GenericMathExtensions to IQuantity (#1448)
Browse files Browse the repository at this point in the history
- replaced the aggregation with an iterator based version (slightly
faster)
- the Average extension now throws an `InvalidOperationException` when
the source is empty
  • Loading branch information
lipchev authored Dec 15, 2024
1 parent 095c85b commit 17e3bfc
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 39 deletions.
54 changes: 48 additions & 6 deletions UnitsNet.Tests/GenericMathExtensionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.

#if NET7_0_OR_GREATER
using System;
using System.Collections.Generic;
using UnitsNet.GenericMath;
using Xunit;

Expand All @@ -10,19 +12,59 @@ namespace UnitsNet.Tests;
public class GenericMathExtensionsTests
{
[Fact]
public void CanCalcSum()
public void Sum_Empty_ReturnsTheAdditiveIdentity()
{
Length[] values = { Length.FromCentimeters(100), Length.FromCentimeters(200) };
Length[] values = [];

Assert.Equal(Length.FromCentimeters(300), values.Sum());
Assert.Equal(Length.Zero, GenericMathExtensions.Sum(values));
}

[Fact]
public void CanCalcAverage_ForQuantitiesWithDoubleValueType()
public void Sum_OneQuantity_ReturnsTheSameQuantity()
{
Length[] values = { Length.FromCentimeters(100), Length.FromCentimeters(200) };
IEnumerable<Length> values = [Length.FromCentimeters(100)];

Assert.Equal(Length.FromCentimeters(150), values.Average());
Length sumOfQuantities = GenericMathExtensions.Sum(values);

Assert.Equal(Length.FromCentimeters(100), sumOfQuantities);
}

[Fact]
public void Sum_TwoQuantities_ReturnsTheExpectedSum()
{
IEnumerable<Length> values = [Length.FromCentimeters(100), Length.FromCentimeters(200)];

Length sumOfQuantities = GenericMathExtensions.Sum(values);

Assert.Equal(Length.FromCentimeters(300), sumOfQuantities);
}

[Fact]
public void Average_Empty_ThrowsInvalidOperationException()
{
IEnumerable<Length> values = [];

Assert.Throws<InvalidOperationException>(() => GenericMathExtensions.Average(values));
}

[Fact]
public void Average_OneQuantity_ReturnsTheSameQuantity()
{
IEnumerable<Length> values = [Length.FromCentimeters(100)];

Length averageOfQuantities = GenericMathExtensions.Average(values);

Assert.Equal(Length.FromCentimeters(100), averageOfQuantities);
}

[Fact]
public void Average_TwoQuantities_ReturnsTheExpectedAverage()
{
IEnumerable<Length> values = [Length.FromCentimeters(100), Length.FromCentimeters(200)];

Length averageOfQuantities = GenericMathExtensions.Average(values);

Assert.Equal(Length.FromCentimeters(150), averageOfQuantities);
}
}

Expand Down
80 changes: 47 additions & 33 deletions UnitsNet/GenericMath/GenericMathExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,67 +2,81 @@
// Copyright 2013 Andreas Gullberg Larsen ([email protected]). Maintained at https://github.com/angularsen/UnitsNet.

#if NET7_0_OR_GREATER
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;

namespace UnitsNet.GenericMath;

/// <summary>
/// Provides generic math operations to test out the new generic math interfaces implemented in .NET7 for UnitsNet
/// quantities using <see cref="double" /> as the internal value type, which is the majority of quantities.
/// Provides generic math operations using the generic math interfaces implemented in .NET7 for UnitsNet.
/// </summary>
public static class GenericMathExtensions
{
/// <summary>
/// Returns the sum of values.
/// Returns the sum of a sequence of vector quantities, such as Mass and Length.
/// </summary>
/// <param name="source">The values.</param>
/// <typeparam name="TQuantity">The type of the quantity elements in the source sequence.</typeparam>
/// <returns>The sum of the quantities, using the unit of the first element in the sequence.</returns>
/// <remarks>
/// This method is experimental and intended to test out the new generic math interfaces implemented in .NET7 for
/// UnitsNet quantities.<br />
/// Generic math interfaces might replace <see cref="UnitMath" />.<br />
/// Generic math LINQ support is still missing in the BCL, but is being worked on:
/// Note that the generic math LINQ support is still missing in the BCL, but is being worked on:
/// <a href="https://github.com/dotnet/runtime/issues/64031">
/// API Proposal: Generic LINQ Numeric Operators · Issue #64031 · dotnet/runtime
/// API Proposal: Generic LINQ Numeric Operators · Issue
/// #64031 · dotnet/runtime
/// </a>
/// </remarks>
/// <param name="source">The values.</param>
/// <typeparam name="T">The type of value.</typeparam>
/// <returns>The sum.</returns>
public static T Sum<T>(this IEnumerable<T> source)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
public static TQuantity Sum<TQuantity>(this IEnumerable<TQuantity> source)
where TQuantity : IQuantity, IAdditionOperators<TQuantity, TQuantity, TQuantity>, IAdditiveIdentity<TQuantity, TQuantity>
{
// Put accumulator on right hand side of the addition operator to construct quantities with the same unit as the values.
// The addition operator implementation picks the unit from the left hand side, and the additive identity (e.g. Length.Zero) is always the base unit.
return source.Aggregate(T.AdditiveIdentity, (acc, item) => item + acc);
using IEnumerator<TQuantity> e = source.GetEnumerator();
if (!e.MoveNext())
{
return TQuantity.AdditiveIdentity;
}

TQuantity result = e.Current;
while (e.MoveNext())
{
result += e.Current;
}

return result;
}

/// <summary>
/// Returns the average of values.
/// Calculates the arithmetic average of a sequence of vector quantities, such as Mass and Length.
/// </summary>
/// <param name="source">The values.</param>
/// <typeparam name="TQuantity">The type of the quantity elements in the source sequence.</typeparam>
/// <returns>The average of the quantities, using the unit of the first element in the sequence.</returns>
/// <exception cref="InvalidOperationException">Thrown when the sequence is empty.</exception>
/// <remarks>
/// This method is experimental and intended to test out the new generic math interfaces implemented in .NET7 for
/// UnitsNet quantities.<br />
/// Generic math interfaces might replace <see cref="UnitMath" />.<br />
/// Generic math LINQ support is still missing in the BCL, but is being worked on:
/// Note that the generic math LINQ support is still missing in the BCL, but is being worked on:
/// <a href="https://github.com/dotnet/runtime/issues/64031">
/// API Proposal: Generic LINQ Numeric Operators · Issue
/// #64031 · dotnet/runtime
/// </a>
/// </remarks>
/// <param name="source">The values.</param>
/// <typeparam name="T">The value type.</typeparam>
/// <returns>The average.</returns>
public static T Average<T>(this IEnumerable<T> source)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>, IDivisionOperators<T, double, T>
public static TQuantity Average<TQuantity>(this IEnumerable<TQuantity> source)
where TQuantity : IQuantity, IAdditionOperators<TQuantity, TQuantity, TQuantity>, IAdditiveIdentity<TQuantity, TQuantity>,
IDivisionOperators<TQuantity, double, TQuantity>
{
// Put accumulator on right hand side of the addition operator to construct quantities with the same unit as the values.
// The addition operator implementation picks the unit from the left hand side, and the additive identity (e.g. Length.Zero) is always the base unit.
(T value, int count) result = source.Aggregate(
(value: T.AdditiveIdentity, count: 0),
(acc, item) => (value: item + acc.value, count: acc.count + 1));
using IEnumerator<TQuantity> e = source.GetEnumerator();
if (!e.MoveNext())
{
throw new InvalidOperationException("Sequence contains no elements");
}

TQuantity result = e.Current;
var nbQuantities = 1;
while (e.MoveNext())
{
result += e.Current;
nbQuantities++;
}

return result.value / result.count;
return result / nbQuantities;
}
}
#endif

0 comments on commit 17e3bfc

Please sign in to comment.