Skip to content

Conversation

@lipchev
Copy link
Collaborator

@lipchev lipchev commented Jul 27, 2025

  • refactored the IQuantity interface: removed the Equals with tolerance and the ToString overloads
  • introduced IAffineQuantity (see Temperature), ILogarithmicQuantity (see AmplitudeRatio) and ILinearQuantity (see Mass)
  • introduced extension methods for Equals with tolerance as well as Sum and Average (as applicable)
  • replaced the implementation of the GetHashCode method with a call to a better optimized function in the Comparison class
  • removed the Sum/Average from UnitMath (replaced be the new extension methods using the more concrete definitions)
  • marked the GenericMathExtensions class as [Obsolete] (replaced by the new extension methods)

fixes #1461

…ce and the ToString overloads

- introduced IAffineQuantity (see Temperature), ILogarithmicQuantity (see AmplitudeRatio) and ILinearQuantity (see Mass)
- introduced extension methods for Equals with tolerance as well as Sum/Average
@lipchev
Copy link
Collaborator Author

lipchev commented Jul 27, 2025

@angularsen This is the most minimalist version of the interface, applicable to the current code-base (which relies on the DefaultUnitConverter):

  1. In the 🐲 PR there isn't the need for the (newly added) double As(UnitKey unitKey) method to exist on the interface (or the extending type), currently it's just there as a work-around the inability to use UnitConverter.Default.ConvertValue(quantity, toUnit) from the extension.
  2. I've removed the very generic Equals(IQuantity, IQuantity) (as we have agreed in an earlier discussion)
  3. I've also removed the concrete Equals with tolerance- it is now an extension method. It is possible to have it on the interface - just not on that level (as we need to be able to specify the type of tolerance):

This definition applies only to the ILinearQuantity and ILogarithmicQuantity

        /// <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);

For the IAffineQuantity (e.g. the Temperature) we need to use this definition:

        bool Equals(TSelf? other, TOffset tolerance);

Copy link
Owner

@angularsen angularsen left a comment

Choose a reason for hiding this comment

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

I didn't get to the end and have to run, but here are comments so far.

}

// // TODO see about this (I think the exception should take precedence to the null check, but the QuantityTests disagree)
// if (tolerance is not TOffset toleranceQuantity)
Copy link
Owner

Choose a reason for hiding this comment

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

I agree, argument validation should happen first, so this should throw.

Copy link
Collaborator Author

@lipchev lipchev Jul 28, 2025

Choose a reason for hiding this comment

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

Oh shit, I had completely forgotten about this thing..

Note that originally we had the very generic Equals(IQuantity? other, IQuantity tolerance) and the more specific Equals(TSelf? other, TSelf tolerance)

  1. Do we want to have an extension of type Equals<TQuantity>(this TQuantity quantity, IQuantity? other, IQuantity tolerance) with a type check on both the other and the tolerance, with just the second one throwing (kinda weird IMO)
  2. Or would we rather have Equals<TQuantity>(this TQuantity quantity, IQuantity? other, TQuantity tolerance) (with TOther tolerance for the affine quantities): no exceptions thrown (returns false when other is not null or another type)
  3. Do we want these at all?

Copy link
Owner

@angularsen angularsen Aug 2, 2025

Choose a reason for hiding this comment

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

  1. No, that's weird to me too. I say remove it.
  2. Yes, I think those look useful enough. I assume you meant TOffset tolerance for affine quantities.
  3. I think nr 2 is useful, unless we are already covered elsewhere that I'm not remembering?
  4. It seems a bit redundant to have EqualsAbsolute(), as nr 1 and nr 2 just delegate to it. As far as I can see, the only difference is nullability. Can it simply be inlined? Or just renamed to Equals() as the absolute part in the name makes me think it serves a special purpose.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'm not sure I understand- Equals(TSelf? other, TSelf tolerance) this overload is a given, my 1) 2) 3) were alternatives for the very generic overload Equals(IQuantity? other, IQuantity tolerance). Do you mean you pick option 2) as in (TQuantity, IQuantity, TQuantity) for the non-affine and (TQuantity, IQuantity, TOffset) for the temperature (which is already the case).

Regarding 4) - so you mean you'd like me to make the EqualsAbsolute internal (it's being used by the two other overloads)?

Copy link
Owner

@angularsen angularsen Aug 2, 2025

Choose a reason for hiding this comment

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

Okay I misread, IQuantity and TQuantity reads very similar.

  1. For tolerance equality, I'd generally expect the fully typed TQuantity experience, for both other and tolerance to match this. And TOffset for affine.
  2. But I guess, why not also allow untyped IQuantity and do type checking internally. It may be useful in certain cases and it costs us little to add. The only thing I'm unsure of, is whether it can trip up intellisense, auto-complete and AI coding assistance to not prefer the typed TQuantity.
  3. Rename EqualsAbsolute to Equals, if it works without issues, or make it private if it's only intended to be used by the other extension methods.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fixed in bdf04d5 - please double-check the signatures of the Equals extensions.

Copy link
Owner

@angularsen angularsen Aug 3, 2025

Choose a reason for hiding this comment

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

Ref nr 2, see comment:
bdf04d5#r163347229

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 link doesn't work, but I read it in the notification:

public static bool Equals(this IQuantity quantity, IQuantity? other, IQuantity tolerance)

This is only going to work if only one of the extensions is visible- (IQuantity, IQuantity, IQuantity) would be ambiguous with (TQuantity, TQuantity, TQuantity)..

Copy link
Owner

@angularsen angularsen left a comment

Choose a reason for hiding this comment

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

Final batch of comments

/// </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>
public interface IArithmeticQuantity<TSelf, TUnitType> : IQuantity<TSelf, TUnitType>, ILinearQuantity<TSelf>
Copy link
Owner

Choose a reason for hiding this comment

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

I think it'll be more to clear to name it ILinearArithmeticQuantity, if it's arithmetic is for linear quantities.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Do you mean to rename the IArithmeticQuantity interface (which would be a breaking change) or the ILinearQuantity interface?

Copy link
Owner

@angularsen angularsen Aug 2, 2025

Choose a reason for hiding this comment

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

Gotcha, no I meant renaming IArithmeticQuantity to ILinearArithmeticQuantity since it's now scoped to only linear quantities, where as before it covered all quantities.

Or is it just that we haven't implemented for affine/logarithmic yet, but this may come later?
We can discuss this more later, not important.

lipchev added 2 commits July 29, 2025 03:23
…arison class

- Temperature: removed the [Obsolete] methods
- added the missing tests for the Linear/Affine/Logarithmic extensions
@angularsen
Copy link
Owner

LGTM and ready to merge, if you want to address any of the open comments before you merge just do so.

angularsen pushed a commit that referenced this pull request Aug 2, 2025
As discussed in #1587, these are supposed to go away in `v6`
lipchev added 3 commits August 3, 2025 09:15
- replaced the type of the tolerance parameter of the Equals extension with the concrete quantity type
@lipchev
Copy link
Collaborator Author

lipchev commented Aug 3, 2025

LGTM and ready to merge, if you want to address any of the open comments before you merge just do so.

I've addressed most of the issues, with the exception of the UnitMath for which I'll open a separate issue/PR. Please double-check the signatures of the Equals extensions and if everything looks good, then merge away..

@lipchev
Copy link
Collaborator Author

lipchev commented Aug 3, 2025

Not that anyone cares about size, but in case you're curious- here's the before and after:

Before (netstandard2.0):
2.46 MB (2 580 480 bytes)

After (netstandard2.0):
2.42 MB (2 544 128 bytes)

🐲 PR (nestandard2.0):
1.77 MB (1 864 192 bytes)

Before (net9.0):
2.55 MB (2 682 368 bytes)

After (net9.0):
2.49 MB (2 616 832 bytes)

🐲 PR (net9.0):
1.80 MB (1 888 256 bytes)

@lipchev
Copy link
Collaborator Author

lipchev commented Aug 3, 2025

@angularsen Actually there two more extensions that I could have used here:

    public static TQuantity ToUnit<TQuantity>(this TQuantity quantity, UnitSystem unitSystem)
        where TQuantity : IQuantityOfType<TQuantity>

and

    public static double As<TQuantity>(this TQuantity quantity, UnitSystem unitSystem)
        where TQuantity : IQuantity

tell me if you rather have them here, or in a separate PR.

In the 🐲 I have these in a separate file: UnitConversionExtensions, but I think they could simply go into QuantityExtensions

@angularsen
Copy link
Owner

tell me if you rather have them here, or in a separate PR.

Separate PR, let's get this bad boy merged. Easier to discuss smaller changes.
Regarding Equals, I have one suggestion #1587 (comment), but we can do that in separate PR also.

@angularsen angularsen merged commit 5c49fb0 into angularsen:master Aug 3, 2025
1 check passed
@angularsen
Copy link
Owner

I'll just merge this for now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ILinearQuantity, ILogarithmicQuantity and IAffineQuantity interfaces

2 participants