Closed
Description
Following my comments on the subject in #1200 here is the proposed modification to the IQuantity
interfaces:
ILinearQuantity
- in v6 Release #1200 I used the nameIVectorQuantity
but after some reflection I decided that the wordLinear
is a better fit (in fact, if we look at the conversion expressions- they are all of the formax
, without an offset)
/// <summary>
/// Represents a quantity that has both magnitude and direction, supporting various arithmetic operations and
/// comparisons.
/// </summary>
/// <remarks>
/// This interface defines standard linear arithmetic operations such as addition, subtraction, multiplication, and
/// division.
/// These types of quantities naturally support comparison operations with either absolute or relative tolerance, which
/// is useful for determining equality within a certain margin of error.
/// <para>
/// For more information, see the Wikipedia page on
/// <a href="https://en.wikipedia.org/wiki/Dimensional_analysis#Geometry:_position_vs._displacement">
/// Dimensional
/// Analysis
/// </a>
/// .
/// </para>
/// </remarks>
/// <typeparam name="TSelf">The type that implements this interface.</typeparam>
public interface ILinearQuantity<TSelf> : IQuantityInstance<TSelf>
#if NET7_0_OR_GREATER
, IAdditiveIdentity<TSelf, TSelf>
#endif
where TSelf : ILinearQuantity<TSelf>
{
#if NET7_0_OR_GREATER
/// <summary>
/// The zero value of this quantity.
/// </summary>
static abstract TSelf Zero { get; }
static TSelf IAdditiveIdentity<TSelf, TSelf>.AdditiveIdentity
{
get => TSelf.Zero;
}
#endif
#if EXTENDED_EQUALS_INTERFACE
/// <summary>
/// <para>
/// Compare equality to <paramref name="other"/> given a <paramref name="tolerance"/> for the maximum allowed +/- difference.
/// </para>
/// <example>
/// In this example, the two quantities will be equal if the value of b is within 0.01 of a (0.01m or 1cm).
/// <code>
/// var a = Length.FromMeters(2.0);
/// var b = Length.FromMeters(2.1);
/// var tolerance = Length.FromCentimeters(10);
/// a.Equals(b, tolerance); // true, 2m equals 2.1m +/- 0.1m
/// </code>
/// </example>
/// <para>
/// It is generally advised against specifying "zero" tolerance, due to the nature of floating-point operations.
/// </para>
/// </summary>
/// <param name="other">The other quantity to compare to.</param>
/// <param name="tolerance">The absolute tolerance value. Must be greater than or equal to zero.</param>
/// <returns>True if the absolute difference between the two values is not greater than the specified tolerance.</returns>
bool Equals(TSelf? other, TSelf tolerance);
#endif
}
/// <summary>
/// An <see cref="IQuantity{TSelf, TUnitType}" /> that (in .NET 7+) implements generic math interfaces for arithmetic
/// operations.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TUnitType">The underlying unit enum type.</typeparam>
public interface IArithmeticQuantity<TSelf, TUnitType> : IQuantity<TSelf, TUnitType>, ILinearQuantity<TSelf>
#if NET7_0_OR_GREATER
, IAdditionOperators<TSelf, TSelf, TSelf>
, ISubtractionOperators<TSelf, TSelf, TSelf>
, IMultiplyOperators<TSelf, QuantityValue, TSelf>
, IDivisionOperators<TSelf, QuantityValue, TSelf>
, IUnaryNegationOperators<TSelf, TSelf>
#endif
where TSelf : IArithmeticQuantity<TSelf, TUnitType>
where TUnitType : struct, Enum
{
}
- ILogarithmicQuantity - in
NET7_OR_GREATER
I introduce the staticLogarithmicScalingFactor
, however innetstandard
this needs to either be an instance member ( 😞 ) or alternatively I could introduce an extra interface for it'sQuantityInfo
, overriding the property to something of the sortnew ILogarithmicQuantityInfo<TQuantity> QuantityInfo { get; }
/// <summary>
/// Represents a logarithmic quantity that supports arithmetic operations and implements generic math interfaces
/// (in .NET 7+). This interface is designed for quantities that are logarithmic in nature, such as decibels.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <remarks>
/// Logarithmic quantities are different from linear quantities in that they represent values on a logarithmic scale.
/// This interface extends <see cref="IQuantity{TSelf, TUnitType}" /> and provides additional functionality specific
/// to logarithmic quantities, including arithmetic operations and a logarithmic scaling factor.
/// The logarithmic scale assumed here is base-10.
/// </remarks>
public interface ILogarithmicQuantity<TSelf> : IQuantityInstance<TSelf>
#if NET7_0_OR_GREATER
, IMultiplicativeIdentity<TSelf, TSelf>
#endif
where TSelf : ILogarithmicQuantity<TSelf>
{
#if NET7_0_OR_GREATER
/// <summary>
/// Gets the logarithmic scaling factor used to convert between linear and logarithmic units.
/// This factor is typically 10, but there are exceptions such as the PowerRatio, which uses 20.
/// </summary>
/// <value>
/// The logarithmic scaling factor.
/// </value>
static abstract QuantityValue LogarithmicScalingFactor { get; }
/// <summary>
/// The zero value of this quantity.
/// </summary>
static abstract TSelf Zero { get; }
static TSelf IMultiplicativeIdentity<TSelf, TSelf>.MultiplicativeIdentity => TSelf.Zero;
#else
/// <summary>
/// Gets the logarithmic scaling factor used to convert between linear and logarithmic units.
/// This factor is typically 10, but there are exceptions such as the PowerRatio, which uses 20.
/// </summary>
/// <value>
/// The logarithmic scaling factor.
/// </value>
QuantityValue LogarithmicScalingFactor { get; }
#endif
}
/// <inheritdoc cref="ILogarithmicQuantity{TSelf}"/>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TUnitType">The underlying unit enum type.</typeparam>
public interface ILogarithmicQuantity<TSelf, TUnitType> : IQuantity<TSelf, TUnitType>, ILogarithmicQuantity<TSelf>
#if NET7_0_OR_GREATER
, IAdditionOperators<TSelf, TSelf, TSelf>
, ISubtractionOperators<TSelf, TSelf, TSelf>
, IMultiplyOperators<TSelf, QuantityValue, TSelf>
, IDivisionOperators<TSelf, QuantityValue, TSelf>
, IUnaryNegationOperators<TSelf, TSelf>
#endif
where TSelf : ILogarithmicQuantity<TSelf, TUnitType>
where TUnitType : struct, Enum
{
}
IAffineQuantity
: there is only one of these- theTemperature
with it'sTOffset
being theTemperatureDelta
(also note that the conversion functions here are of the formax + b
)
/// <summary>
/// An <see cref="IQuantity{TSelf}"/> that (in .NET 7+) implements generic math interfaces for arithmetic operations.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TOffset"></typeparam>
public interface IAffineQuantity<TSelf, TOffset> : IQuantityInstance<TSelf>
#if NET7_0_OR_GREATER
, IAdditiveIdentity<TSelf, TOffset>
where TOffset : IAdditiveIdentity<TOffset, TOffset>
#endif
where TSelf : IAffineQuantity<TSelf, TOffset>
{
#if NET7_0_OR_GREATER
/// <summary>
/// The zero value of this quantity.
/// </summary>
static abstract TSelf Zero { get; }
static TOffset IAdditiveIdentity<TSelf, TOffset>.AdditiveIdentity => TOffset.AdditiveIdentity;
#endif
#if EXTENDED_EQUALS_INTERFACE
/// <summary>
/// <para>
/// Compare equality to <paramref name="other"/> given a <paramref name="tolerance"/> for the maximum allowed +/- difference.
/// </para>
/// <example>
/// In this example, the two quantities will be equal if the value of b is within 0.01 of a (0.01m or 1cm).
/// <code>
/// var a = Length.FromMeters(2.0);
/// var b = Length.FromMeters(2.1);
/// var tolerance = Length.FromCentimeters(10);
/// a.Equals(b, tolerance); // true, 2m equals 2.1m +/- 0.1m
/// </code>
/// </example>
/// <para>
/// It is generally advised against specifying "zero" tolerance, due to the nature of floating-point operations.
/// </para>
/// </summary>
/// <param name="other">The other quantity to compare to.</param>
/// <param name="tolerance">The absolute tolerance value. Must be greater than or equal to zero.</param>
/// <returns>True if the absolute difference between the two values is not greater than the specified tolerance.</returns>
bool Equals(TSelf? other, TOffset tolerance);
#endif
}
/// <summary>
/// An <see cref="IQuantity{TSelf, TUnitType}"/> that (in .NET 7+) implements generic math interfaces for arithmetic operations.
/// </summary>
/// <typeparam name="TSelf">The type itself, for the CRT pattern.</typeparam>
/// <typeparam name="TUnitType">The underlying unit enum type.</typeparam>
/// <typeparam name="TOffset"></typeparam>
public interface IAffineQuantity<TSelf, TUnitType, TOffset> : IQuantity<TSelf, TUnitType>, IAffineQuantity<TSelf, TOffset>
#if NET7_0_OR_GREATER
, IAdditionOperators<TSelf, TOffset, TSelf>
, ISubtractionOperators<TSelf, TSelf, TOffset>
where TOffset : IAdditiveIdentity<TOffset, TOffset>
#endif
where TSelf : IAffineQuantity<TSelf, TUnitType, TOffset>
where TUnitType : struct, Enum
{
}
Activity
lipchev commentedon Dec 16, 2024
You'd probably wonder about the
IQuantityInstance<TSelf>
interface - but that's a subject for another time, for now you can think of it as an interface that allows us to constrain a collection ofTQuantity
to be of the same type without having to introduce a second generic parameter, representing theTUnit
:It currently sits between the
IQuantity<TQuantity, TUnit>
andIQuantity
(in the same file - please tell me if you think this should go into it's own .cs):lipchev commentedon Dec 16, 2024
Before I get to the
EXTENDED_EQUALS_INTERFACE
(and why I don't have it enabled on the interface) - let me give you the prototypes of theSum
/Average
extensions (they work in bothnetstandard2.0
andNET8
):LinearQuantityExtensions
:It's basically the same thing for the
Average
only here we're also throwing anInvalidOperationException
when the sequence is empty:All of these extensions take precedence over the extensions in the
GenericMathExtensions
(and they are also faster, as they just sum up theValue
instead of invoking the+
operator on theTQuantity
).The other extensions would apply for "custom quantities that implement the
IAdditiveIdentity
andIAdditiveOperators
but don't explicitly implement ourILinearQuantity
interface"- think "very custom" quantities 😄PS They also replace the same extensions from the
UnitMath
- which should become mostly empty of extensions (seeClamp
and theMin
/Max
- which are not really extensions, and ideally should be moved to theQuantity
class, deprecating the old file completely).lipchev commentedon Dec 16, 2024
LogarithmicQuantityExtensions
:I've also got the rest of the overloads but in order to reduce the clutter, I'm going to just focus on the main ones:
Notice that we're throwing an
InvalidOperationException
when the collection is empty- as there isn't a validAdditiveIdentity
that we could return.Another thing to pay attention to is the use of the
significantDigits
parameter- since the additions need to be performed in linear space there is no way around the possible rounding errors involved with thePow
(non integer) and theLog
functions. I'm very strict about it- every operation that is not exact has this parameter (there aren't many)..Frankly, I wasn't even sure if it makes sense to have this one - but the AI had some convincing arguments (which I no longer recall) - so I kept it.
Again, the AI insisted that both the arithmetic and the geometric means make sense in certain situations, but I cannot confirm or dispute that claim..
lipchev commentedon Dec 16, 2024
AffineQuantityExtensions
only has theAverage
for theTemperature
- I'm not sure if the operation is applicable to the all affine quantities (whoever comes up with another Affine Quantity would have to tell us):lipchev commentedon Dec 16, 2024
Ok lastly, I'm going to try to be short w.r.t. the "Equals with tolerance" - as we know, this is currently the recommended way of comparing all quantities due to the possible rounding error incurred by the unit conversions etc.
Although this particular issue is no longer present in my Fraction-based implementation, I think the method is still somewhat useful provided we:
public bool Equals(Temperature other, Temperature tolerance)
method, replacing the type of thetolerance
with aTemperatureDelta
.Equals
(with the correct tolerance type) out of the interface altogether (same argument as in Remove IComparisonOperators<TSelf, TSelf, bool> and IParsable<TSelf> from the IQuantity interface #1454) and turn it into an extension method in one of the previously mentioned extension classes (LinearQuantityExtensions
,LogarithmicQuantityExtensions
or theAffineQuantityExtensions
). Here is what I've got down for theILinearQuantity
:Ok, so I'm going to stop here for now, if you agree with the overall picture so far- I can create a separate issue regarding the modifications to the Equals (with tolerance) part of the interfaces.. (or of course I can keep going here if you prefer 😆 )..
SupportsSIUnitSystem
property should no be needed #1463angularsen commentedon Dec 21, 2024
A lot to unwrap here.
Name
ILinearQuantity
instead ofIVectorQuantity
- OKLogarithmicScalingFactor
, must we not be backwards compatible with netstandard2.0? Meaning, if we need instance member there, we also need it for net7? I think it makes sense to push this up toQuantityInfo
, which is shared information for all quantities that don't need to follow each quantity instance/value. We could maybe introduce a derivedLogarithmicQuantityInfo
class that extendsQuantityInfo
, we might not need to sprinkle yet more interfaces over this.IAffineQuantity
, looks goodPerformance improvement of Sum/Average by passing in a unit that the quantities mostly have, sounds good. Feels a bit niche, but probably helps in some cases.
I don't think we should cater to this use case, unless it costs us nothing to add and maintain this. I have no problem requiring custom quantities to implement either
ILinearQuantity
orILogarithmicQuantity
to benefit from our generic math, and avoid pitfalls like Temperature with generic math.significantDigits
- I don't quite understand this one. I get there will be rounding errors, but why do we require/need to specify the significant digits to retain, can't we just retain the max and not perform any rounding?This is a bit of red flag for me, okay orange. I think it's very important that we only add stuff that will actually be used, and ideally will be used by the implementer so we have a good chance of ensuring it is correct and makes sense in whatever domain. I really want to avoid adding stuff just because we can or hoping someone might find it useful. Similar to the
IConvertible
discussion earlier, and the initial generic math interface implementation although that was intentionally experimental and for exploration. If you are unsure about whether we should add something, then I think we should generally favor not doing it.Temperature equals changes - agreed
Not sure about this one, can't we do it generically with
IAffineQuantity
without limiting to just Temperature?That's all I got time for right now. Generally though, I think the changes look good 👍
angularsen commentedon Dec 21, 2024
Better to continue discussion for each change's pull request, instead of in a mega thread.
lipchev commentedon Dec 21, 2024
The downside here is that the
LogarithmicQuantityInfo
would have to be anILogarithmicQuantityInfo
- as we cannot extend the base classQuantityInfo<Unit>
(I don't remember the exact reason anymore but I'm pretty sure it couldn't be a class).That's just what the comment says- in reality I'm keeping it mostly in order to avoid the breaking change.
I think we should keep it for now, even if only to avoid the breaking change (for the extended quantities).
The default is 15 significant digits - which is the safe maximum precision that avoids the rounding error (when converting a
double
toQuantityValue
). If you specify a lower number of significant digits (e.g. 4), the performance is better (depends on the numbers of course).I'm pretty sure that the calculations for the
LogarithmicSum
,ArithmeticMean
/GeometricMean
are correct - I'm mostly not (how / if) they would be useful. I initially thought of just "not breaking" the existing usages, but then wasn't sure which average should be used by default.In terms of code- it's not insignificant (I just finished covering the tests for the
RootN
) and frankly some of it is probably not that fast- I don't care much about the LogQuantities myself, I just figured if anyone does use such extensions, they might as well use the more optimized version..Technically there is also the possibility to calculate the RootN with 500 digits of precision but that's probably a more extreme scenario
Technically we can but:
Note that unfortunately I won't be able to bring these changes in without introducing the
Unsafe
and all the rest of it..What do you think about the
Equals
with tolerance- do you agree about turning it into an extension method (removing it from the interface)? Should I open another issue / discussion about it?lipchev commentedon Dec 21, 2024
Ok, that's not 100% accurate - I think I could introduce them with as a standalone PR, however the standard extensions (without the
Unit
) are going to be much slower than before (we'd have to be using the untypedAs(Enum)
).angularsen commentedon Dec 21, 2024
Equals as extension method is OK I think, as long is it's in the
UnitsNet
namespace and easily discoverable.Regarding
Unsafe
, I don't recall the details there but this was a separate nuget and primarily a performance benefit? Let's discuss that more in a PR where we have some context.github-actions commentedon Jan 21, 2025
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.
7 remaining items
lipchev commentedon Mar 30, 2025
I was able to refactor the following
IQuantity
methods as public extensions:public static QuantityValue As<TQuantity, TUnit>(this TQuantity quantity, TUnit unit)
public static QuantityValue As<TQuantity>(this TQuantity quantity, UnitSystem unitSystem)
public static TQuantity ToUnit<TQuantity, TUnit>(this TQuantity quantity, TUnit unit)
public static TQuantity ToUnit<TQuantity, TUnit>(this TQuantity quantity, TUnit unit, UnitConverter unitConverter)
public static TQuantity ToUnit<TQuantity>(this TQuantity quantity, UnitSystem unitSystem)
public static string ToString<TQuantity>(this TQuantity quantity, IFormatProvider? formatProvider)
public static string ToString<TQuantity>(this TQuantity quantity, string? format)
public static bool EqualsAbsolute<TQuantity, TOther, TTolerance>(this TQuantity quantity, TOther other, TTolerance tolerance)
(arithmetic)public static bool EqualsAbsolute<TQuantity, TOther, TTolerance>(this TQuantity quantity, TOther other, TTolerance tolerance)
(logarithmic)public static bool EqualsAbsolute<TQuantity, TOffset>(this TQuantity quantity, TQuantity other, TOffset tolerance)
(affine)This has removed a lot of code from our quantities (as well as the
HowMuch
)- we're currently at1.75 MB (1 845 248 bytes)
(fornet8.0
).However there is downside- the extension methods are showing up in the auto-completion drop-down, for all types- not just those derived from
IQuantity
(as stated by the constraint). Of course, if you were to auto-complete the statement, this wouldn't compile, but yeah- for some reason visual studio isn't filtering them properly (maybe this would be fixed in the future, or maybe it's actually an issue with resharper- I don't know).Of course they won't show up unless the
UnitsNet
import is present, and they do seem to be collapsed into just 4 methods (As
,ToUnit
,ToString
,EqualsAbsolute
), but yeah.. 😒lipchev commentedon Mar 30, 2025
It's actually only those with two generic parameters that are showing up where they aren't supposed to,
ToString
isn't showing up on other types.If I were to add the
struct
constraint forTQuantity
, that appears to filter correctly for classes, but still not ideal... (besides I've always wanted to benchmark a class-based implementation ofHowMuch
...).lipchev commentedon Apr 2, 2025
The good news is that the issue doesn't occur in Visual Studio unless the
ReSharper
extension is active. I think you know what the bad news is:https://youtrack.jetbrains.com/issue/RSRP-500459/Extension-methods-with-two-generic-parameters-are-not-filtered
I only opened the ticket yesterday, so it still hasn't passed QA, but it seems like a legit issue - please up-vote it, if you agree.
angularsen commentedon Apr 2, 2025
Seems like a bug to me, upvoted.
I use Rider, but it probably would have the same issue as it uses ReSharper engine.
angularsen commentedon Apr 2, 2025
We can hold off on this until clarification in the youtrack issue, but I assume it will be considered a bug.
Also, I'll have to find some time to get to reviewing this one as it is blocking:
#1507
angularsen commentedon Apr 12, 2025
#1507 is merged, so this is unblocked.
I tried your intellisense issue in Rider, but can't reproduce. I guess it's isolated to VS+ReSharper.
lipchev commentedon Apr 14, 2025
There was another small issue that I noticed while updating the nugget in our code-base: the extension method requires a namespace import, where one might not have been necessary before.
Here's an example:
This is quite an unlikely scenario, and the IDE is quick to suggest the correct import (
UnitsNet.Units
), so I don't think this is deal-breaker, but still- I wanted to put it out there.PS Hopefully this also clears up the reason why in #1544 I have two sets of extensions -
QuantityExtensions
and theUnitConversionExtensions
in theUnits
namespace.PS2 I just realized that I forgot to put the extensions together in the same solution folder (
UnitsNet/Extensions
?)..lipchev commentedon Apr 17, 2025
Did you see my comment on the issue tracker? Am I misunderstanding something, and if not do you think we should postpone this or not?
PS You've probably seen the news about NET10 release (which I hope we'd be able to ship ahead of this time 😆 ) - we're getting the good stuff, things like
Mass.GetDefaultAbbreviation(..)
should also work as an extension.angularsen commentedon Apr 18, 2025
Brain fart on my part, it's totally an issue in Rider too.
I guess, since it only affects code where you use UnitsNet and have namespace usings for UnitsNet, then the bug is less of an issue. And I suppose it will eventually be fixed as well.
And it does not affect Visual Studio without ReSharper, so there's that too.
As long as existing UnitsNet consumers' code will continue to work without any changes, then extension methods sounds good to me.
github-actions commentedon May 19, 2025
This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 7 days.
github-actions commentedon May 27, 2025
This issue was automatically closed due to inactivity.