Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constrained the GenericMathExtensions to IQuantity #1448

Merged
merged 1 commit into from
Dec 15, 2024
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
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);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I'm not using values.Sum() is because when we add the IVectorQuantity interface, this is going to start testing the other extension method (which is a better/more specific match for the list of Length).


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");
}
Comment on lines +66 to +69
Copy link
Collaborator Author

@lipchev lipchev Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice this: in v5 this case would have caused an exception to be thrown because of the NaN, however as we disabled the guard in v6- this would not have thrown an exception.
This is also the behavior of Average(IEnumerable<Double>).


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

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