-
Notifications
You must be signed in to change notification settings - Fork 382
Experimental: Generic Math and INumber
Generic math was introduced in .NET 7.
https://learn.microsoft.com/en-us/dotnet/standard/generics/math
We wanted to see what works and what doesn't for Units.NET, so we added some experimental support for the generic math interfaces in
Generic math for UnitsNet in .NET 7 · Pull Request #1164.
Today, 3 quantities (Information, BitRate, Power
) use decimal
, all other 120+ quantities use double
.
With generics, the consumer would be able to choose the numeric type, including new types like float
.
Sum and Average, for now.
[Fact]
public void CanCalcSum()
{
Length[] values = { Length.FromCentimeters(100), Length.FromCentimeters(200) };
Assert.Equal(Length.FromCentimeters(300), values.Sum());
}
[Fact]
public void CanCalcAverage_ForQuantitiesWithDoubleValueType()
{
Length[] values = { Length.FromCentimeters(100), Length.FromCentimeters(200) };
Assert.Equal(Length.FromCentimeters(150), values.Average());
}
It seems there are no implementations shipped with .NET yet, so we provide these two extension methods as a proof of concept. We can add more if there is a need for it.
https://github.com/angularsen/UnitsNet/blob/master/UnitsNet/GenericMath/GenericMathExtensions.cs
public static T Sum<T>(this IEnumerable<T> source)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>
{
// 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);
}
public static T Average<T>(this IEnumerable<T> source)
where T : IAdditionOperators<T, T, T>, IAdditiveIdentity<T, T>, IDivisionOperators<T, double, T>
{
// 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));
return result.value / result.count;
}
UnitsNet does not provide Min/Max values for quantities, since you quickly run into overflow exceptions when converting to other units. Also the Min/Max could change when introducing bigger/smaller units.
Temperature has its own quirks with arithmetic in general.
0 Celsius != 0 Fahrenheit != 0 Kelvin.
So for example 20 °C + 5 °C is ambiguous. It could mean 25 °C, or it could mean 293.15 K + 278.15 K.
This made it hard to implement IAdditiveIdentity in a way that is intuitive, which is essential to arithmetic like Average()
.
We previously introduced TemperatureDelta
to make arithmetic more clear, but this is not compatible with generic math.
I was not able to create a truly generic Average()
for both double
and decimal
, the compiler would complain about ambiguity, so I wound up with DecimalGenericMathExtensions.Average<T>
. It doesn't mean it can't be done, but I could not figure it out.
We can't implement INumber<>
, but we can implement some individual generic math interfaces like IAdditionOperators
, IMultiplyOperators
, IAdditiveIdentity
.
Units can trip up things like Average()
using IAdditiveIdentity
as the starting value, which typically maps to Length.Zero
. Since addition picks the left hand side unit, the addition argument order must be so that the accumulated value is on the right hand side, to avoid getting the base unit instead of the first item's unit.
- Can we avoid having to specify the numeric type
Length<double>
everywhere?- We could have a default
Length
and a genericLength<T>
, where the default was like today withdouble
for all or most quantities. Due tostruct
not supporting inheritance, we would have to create twice as many quantities as today. - If changed from
struct
toclass
, we could create derived types in different namespaces, likeUnitsNet.Double.Length
andUnitsNet.Decimal.Length
deriving fromLength<T>
.
- We could have a default