Skip to content

Enum boxing optimizations (UnitKey) #1507

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

Merged
merged 7 commits into from
Apr 11, 2025
Merged

Conversation

lipchev
Copy link
Collaborator

@lipchev lipchev commented Jan 31, 2025

Enum boxing optimizations:

  • introduce the UnitKey struct as a replacement for the conversions from TUnit to Enum
  • re-mapped the UnitAbbreviationCache using the UnitKey for the concurrent dictionary
  • removed all Convert.ToInt32(..) calls
  • optimized the QuantityInfoLookup collections
  • refactored some of the Quantity/UnitConverter code (using the UnitParser/QuantityInfoLookup)
  • added some tests and benchmarks covering the new modifications

Breaking changes:

  • changing the Quantity.Names from string[] to IReadOnlyCollection<string>
  • changing the Quantity.Infos from QuantityInfo[] to IReadOnlyList<QuantityInfo>
  • marking the UnitConverter.ConvertByAbbreviation method that takes a string? for the IFormatProvider? as [Obsolete] (created an overload)
  • UnitsNetSetup: replaced the ICollection<QuantityInfo> constructor parameter with IEnumerable<QuantityInfo>
  • renamed the QuantityInfo.ValueType to QuantityInfo.QuantityType (making the old property [Obsolete])
  • renamed two of the parameters of UnitConverter.ConvertByName

- introduce the UnitKey struct as a replacement for the conversions from TUnit to Enum
- re-map the UnitAbbreviationCache using the UnitKey for the concurrent dictionary
- remove all Convert.ToInt32(..) calls
- optimize the QuantityInfoLookup collections
- refactored some of the Quantity/UnitConverter code (using the UnitParser/UnitAbbreviationsCache)
- added some tests and benchmarks covering the new modifications
@lipchev lipchev changed the title Enum boxing optimizations: Enum boxing optimizations (UnitKey) Jan 31, 2025
public static QuantityInfo[] Infos => Default.Infos;
public static IReadOnlyCollection<QuantityInfo> Infos => Quantities.Infos;
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is the second breaking change: same deal- can't have users setting the values of the array.

I would also like to make Quantity.ByName dictionary read-only:

        /// <summary>
        /// All QuantityInfo instances mapped by quantity name that are present in the <see cref="UnitsNetSetup.Default"/> configuration.
        /// </summary>
        public static IReadOnlyDictionary<string, QuantityInfo> ByName => Quantities.ByName;

The generated part of the Quantity class should only contain a single collection of QuantityInfo that we should pass to the QuantityInfoLookup. Here's what I have in the other project:

public partial class Quantity
{
    /// <summary>
    ///     Serves as a repository for predefined quantity conversion mappings, facilitating the automatic generation and retrieval of unit conversions in the UnitsNet library.
    /// </summary>
    internal static class Provider
    {
        /// <summary>
        ///     All QuantityInfo instances that are present in UnitsNet by default.
        /// </summary>
        internal static IReadOnlyCollection<QuantityInfo> DefaultQuantities => new QuantityInfo[]
        {
            AbsorbedDoseOfIonizingRadiation.Info,

Comment on lines +12 to +26
/// <summary>
/// Get a list of quantities having the given base dimensions.
/// </summary>
/// <param name="quantityInfos">The type of quantity mapping information.</param>
/// <param name="baseDimensions">The base dimensions to match.</param>
public static IEnumerable<QuantityInfo> GetQuantitiesWithBaseDimensions(this IEnumerable<QuantityInfo> quantityInfos,
BaseDimensions baseDimensions)
{
if (baseDimensions is null)
{
throw new ArgumentNullException(nameof(baseDimensions));
}

return quantityInfos.Where(info => info.BaseDimensions.Equals(baseDimensions));
}
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This extensions doesn't have to be public: it's currently only used from the Quantity class.

Comment on lines -73 to +84
/// Quantity value type, such as <see cref="Length"/> or <see cref="Mass"/>.
/// Quantity value type, such as <see cref="Length" /> or <see cref="Mass" />.
/// </summary>
public Type ValueType { get; }
public Type QuantityType { get; }

/// <inheritdoc cref="QuantityType" />
[Obsolete("Replaced by the QuantityType property.")]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public Type ValueType
{
get => QuantityType;
}
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 hope you don't mind- this has been bugging me for ages...

Copy link
Owner

Choose a reason for hiding this comment

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

Agreed, but why not just remove it since this is targeting v6?
We could deprecate in v5 instead and offer a similar change there.

Comment on lines +427 to +459
[Obsolete("Methods accepting a culture name are deprecated in favor of using an instance of the IFormatProvider.")]
public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, string? culture)
{
if (!TryGetUnitType(quantityName, out Type? unitType))
throw new UnitNotFoundException($"The unit type for the given quantity was not found: {quantityName}");

var cultureInfo = CultureHelper.GetCultureOrInvariant(culture);

var fromUnit = UnitsNetSetup.Default.UnitParser.Parse(fromUnitAbbrev, unitType, cultureInfo); // ex: ("m", LengthUnit) => LengthUnit.Meter
var fromQuantity = Quantity.From(fromValue, fromUnit);
return ConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, CultureHelper.GetCultureOrInvariant(culture));
}

var toUnit = UnitsNetSetup.Default.UnitParser.Parse(toUnitAbbrev, unitType, cultureInfo); // ex:("cm", LengthUnit) => LengthUnit.Centimeter
return fromQuantity.As(toUnit);
/// <summary>
/// Convert between any two quantity units by their abbreviations, such as converting a "Length" of N "m" to "cm".
/// This is particularly useful for creating things like a generated unit conversion UI,
/// where you list some selectors:
/// a) Quantity: Length, Mass, Force etc.
/// b) From unit: Meter, Centimeter etc if Length is selected
/// c) To unit: Meter, Centimeter etc if Length is selected
/// </summary>
/// <param name="fromValue">
/// Input value, which together with <paramref name="fromUnitAbbrev" /> represents the quantity to
/// convert from.
/// </param>
/// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
/// <param name="fromUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
/// <param name="toUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
/// <param name="formatProvider">
/// The format provider to use for lookup. Defaults to <see cref="System.Globalization.CultureInfo.CurrentCulture" />
/// if null.
/// </param>
/// <example>double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500</example>
/// <returns>Output value as the result of converting to <paramref name="toUnitAbbrev" />.</returns>
/// <exception cref="QuantityNotFoundException">
/// Thrown when no quantity information is found for the specified quantity name.
/// </exception>
/// <exception cref="UnitNotFoundException">No units match the abbreviation.</exception>
/// <exception cref="AmbiguousUnitParseException">More than one unit matches the abbreviation.</exception>
public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, IFormatProvider? formatProvider)
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've added an IFormatProvider? overload and deprecated the whole CultureHelper class:

string -> CultureInfo conversions are not in the scope of UnitsNet

If you want to- we could also make this for v5 and have the string? overload directly removed in v6.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Oh I think I see what the deal is- the UnitParser doesn't really care about the IFormat part of the IFormatProvider, just the CultureName.. I wonder if there is a more standard way of dealing with things that depend solely on the language / region (and not that number-formatting stuff).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Given that we ultimately depend on the ResourceManager, and it takes a CultureInfo? as it's second parameter, I think the right way (another breaking change) would be to make everything that doesn't involve formatting the number (i.e. everything in the UnitParser, UnitAbbreviationsCache, as well as this new overload that I just added) use a CultureInfo? (with the rest of the code safe-casting the IFormatProvider?)..

Copy link
Owner

Choose a reason for hiding this comment

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

I was about to comment on the same thing reading the above obsoletion of string-based culture names.

I'm not sure how I feel about string vs IFormatProvider, since we don't really use the culture formatting for anything, we just rely on the culture name.

If we end up just getting cached CultureInfo instances via CultureInfo.GetCultureInfo(name) from these strings anyway, I guess we could argue to pass in those instance to begin with.
And yes, it makes more sense to pass in CultureInfo here rather than IFormatProvider + safe-casting.

Just to mention, ResourceManager has some built-in logic for locale fallback: https://learn.microsoft.com/en-us/globalization/locale/fallback

@lipchev
Copy link
Collaborator Author

lipchev commented Jan 31, 2025

Oh, I just remembered that there are two more benchmarks that I forgot to post:

ParseUnitBenchmarks.cs

Before:

Method Job Runtime Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
ParseMassUnit .NET 8.0 .NET 8.0 6.746 ms 0.0381 ms 0.0338 ms 1.00 0.01 640.6250 - 10.32 MB 1.00
ParseVolumeUnit .NET 8.0 .NET 8.0 14.145 ms 0.0945 ms 0.0884 ms 2.10 0.02 1109.3750 - 17.8 MB 1.72
ParseDensityUnit .NET 8.0 .NET 8.0 15.158 ms 0.1904 ms 0.1781 ms 2.25 0.03 1140.6250 - 18.38 MB 1.78
ParsePressureUnit .NET 8.0 .NET 8.0 13.221 ms 0.0667 ms 0.0624 ms 1.96 0.01 1031.2500 - 16.62 MB 1.61
ParseVolumeFlowUnit .NET 8.0 .NET 8.0 22.251 ms 0.3512 ms 0.3285 ms 3.30 0.05 1906.2500 31.2500 30.44 MB 2.95
                       
ParseMassUnit .NET Framework 4.8 .NET Framework 4.8 21.608 ms 0.0759 ms 0.0634 ms 1.00 0.00 2531.2500 - 15.34 MB 1.00
ParseVolumeUnit .NET Framework 4.8 .NET Framework 4.8 44.665 ms 0.3540 ms 0.2956 ms 2.07 0.01 4727.2727 - 28.73 MB 1.87
ParseDensityUnit .NET Framework 4.8 .NET Framework 4.8 47.236 ms 0.6410 ms 0.5996 ms 2.19 0.03 5181.8182 - 31.25 MB 2.04
ParsePressureUnit .NET Framework 4.8 .NET Framework 4.8 41.511 ms 0.3631 ms 0.3219 ms 1.92 0.02 4583.3333 - 27.62 MB 1.80
ParseVolumeFlowUnit .NET Framework 4.8 .NET Framework 4.8 66.566 ms 1.2644 ms 1.1827 ms 3.08 0.05 7875.0000 125.0000 47.4 MB 3.09

After:

Method Job Runtime Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
ParseMassUnit .NET 8.0 .NET 8.0 2.729 ms 0.0427 ms 0.0356 ms 1.00 0.02 472.6563 3.9063 7.58 MB 1.00
ParseVolumeUnit .NET 8.0 .NET 8.0 5.144 ms 0.1026 ms 0.2861 ms 1.89 0.11 765.6250 7.8125 12.27 MB 1.62
ParseDensityUnit .NET 8.0 .NET 8.0 5.304 ms 0.0211 ms 0.0197 ms 1.94 0.03 796.8750 7.8125 12.82 MB 1.69
ParsePressureUnit .NET 8.0 .NET 8.0 4.820 ms 0.0156 ms 0.0122 ms 1.77 0.02 726.5625 7.8125 11.64 MB 1.54
ParseVolumeFlowUnit .NET 8.0 .NET 8.0 7.703 ms 0.1002 ms 0.0836 ms 2.82 0.05 1406.2500 31.2500 22.54 MB 2.97
                       
ParseMassUnit .NET Framework 4.8 .NET Framework 4.8 7.085 ms 0.0185 ms 0.0173 ms 1.00 0.00 1390.6250 7.8125 8.36 MB 1.00
ParseVolumeUnit .NET Framework 4.8 .NET Framework 4.8 13.392 ms 0.0400 ms 0.0354 ms 1.89 0.01 2359.3750 15.6250 14.2 MB 1.70
ParseDensityUnit .NET Framework 4.8 .NET Framework 4.8 13.938 ms 0.1321 ms 0.1171 ms 1.97 0.02 2406.2500 15.6250 14.52 MB 1.74
ParsePressureUnit .NET Framework 4.8 .NET Framework 4.8 12.253 ms 0.1216 ms 0.1137 ms 1.73 0.02 2187.5000 15.6250 13.18 MB 1.58
ParseVolumeFlowUnit .NET Framework 4.8 .NET Framework 4.8 19.720 ms 0.2704 ms 0.2530 ms 2.78 0.04 4187.5000 93.7500 25.2 MB 3.02

@lipchev
Copy link
Collaborator Author

lipchev commented Jan 31, 2025

TryParseInvalidUnitBenchmarks.cs

Before:

Method Job Runtime Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
TryParseMassUnit .NET 8.0 .NET 8.0 7.250 ms 0.0933 ms 0.0827 ms 1.00 0.02 656.2500 - 10.51 MB 1.00
TryParseVolumeUnit .NET 8.0 .NET 8.0 15.267 ms 0.2171 ms 0.2031 ms 2.11 0.04 1109.3750 15.6250 17.92 MB 1.70
TryParseDensityUnit .NET 8.0 .NET 8.0 15.942 ms 0.1558 ms 0.1301 ms 2.20 0.03 1156.2500 - 18.57 MB 1.77
TryParsePressureUnit .NET 8.0 .NET 8.0 13.440 ms 0.0523 ms 0.0489 ms 1.85 0.02 1046.8750 - 16.79 MB 1.60
TryParseVolumeFlowUnit .NET 8.0 .NET 8.0 22.725 ms 0.3670 ms 0.3433 ms 3.13 0.06 1906.2500 31.2500 30.59 MB 2.91
                       
TryParseMassUnit .NET Framework 4.8 .NET Framework 4.8 22.157 ms 0.0661 ms 0.0618 ms 1.00 0.00 2593.7500 - 15.58 MB 1.00
TryParseVolumeUnit .NET Framework 4.8 .NET Framework 4.8 45.670 ms 0.3925 ms 0.3480 ms 2.06 0.02 4750.0000 - 28.9 MB 1.85
TryParseDensityUnit .NET Framework 4.8 .NET Framework 4.8 48.609 ms 0.0723 ms 0.0604 ms 2.19 0.01 5181.8182 - 31.49 MB 2.02
TryParsePressureUnit .NET Framework 4.8 .NET Framework 4.8 42.079 ms 0.1119 ms 0.1047 ms 1.90 0.01 4583.3333 - 27.85 MB 1.79
TryParseVolumeFlowUnit .NET Framework 4.8 .NET Framework 4.8 68.444 ms 0.1200 ms 0.0937 ms 3.09 0.01 7875.0000 125.0000 47.61 MB 3.06

After:

Method Job Runtime NbAbbreviations Mean Error StdDev Ratio RatioSD Gen0 Gen1 Allocated Alloc Ratio
TryParseMassUnit .NET 8.0 .NET 8.0 1000 3.116 ms 0.0327 ms 0.0273 ms 1.00 0.01 484.3750 3.9063 7.77 MB 1.00
TryParseVolumeUnit .NET 8.0 .NET 8.0 1000 5.424 ms 0.0640 ms 0.0629 ms 1.74 0.02 773.4375 7.8125 12.4 MB 1.60
TryParseDensityUnit .NET 8.0 .NET 8.0 1000 5.808 ms 0.1109 ms 0.1139 ms 1.86 0.04 812.5000 7.8125 13.01 MB 1.68
TryParsePressureUnit .NET 8.0 .NET 8.0 1000 5.175 ms 0.0920 ms 0.0816 ms 1.66 0.03 734.3750 7.8125 11.81 MB 1.52
TryParseVolumeFlowUnit .NET 8.0 .NET 8.0 1000 8.070 ms 0.1596 ms 0.2437 ms 2.59 0.08 1421.8750 31.2500 22.69 MB 2.92
                         
TryParseMassUnit .NET Framework 4.8 .NET Framework 4.8 1000 7.812 ms 0.0085 ms 0.0071 ms 1.00 0.00 1429.6875 7.8125 8.6 MB 1.00
TryParseVolumeUnit .NET Framework 4.8 .NET Framework 4.8 1000 14.426 ms 0.1973 ms 0.1846 ms 1.85 0.02 2390.6250 15.6250 14.38 MB 1.67
TryParseDensityUnit .NET Framework 4.8 .NET Framework 4.8 1000 14.768 ms 0.0424 ms 0.0354 ms 1.89 0.00 2453.1250 15.6250 14.77 MB 1.72
TryParsePressureUnit .NET Framework 4.8 .NET Framework 4.8 1000 13.276 ms 0.0195 ms 0.0182 ms 1.70 0.00 2234.3750 15.6250 13.41 MB 1.56
TryParseVolumeFlowUnit .NET Framework 4.8 .NET Framework 4.8 1000 20.428 ms 0.0411 ms 0.0343 ms 2.62 0.00 4218.7500 93.7500 25.41 MB 2.95

Eh, yeah - I've made the number of iterations a constant 1000 but didn't bother to update the result (I used to think it would make sense to provide the number of iterations in the report).

…r with IReadOnlyCollection<QuantityInfo>

- UnitsNetSetup: replaced the ICollection<QuantityInfo> parameter with IReadOnlyCollection<QuantityInfo>
- UnitAbbreviationsCache: introduced a constructor with a list of quantities, and improve the comments of the other constructors
- added tests and benchmarks covering the UnitAbbreviations initializations
…sing backing fields)

- added some benchmarks for the Enum/UnitKey
- updated some typos for the TryParseUnitBenchmarks and introduced an explict Culture for the Parse/TryParse unit benchmarks
…d the _quantitiesByName into a regular (lazy-loaded) dictionary

- UnitsNetSetup and UnitAbbreviationsCache constructors changed from IReadOnlyCollection<QuantityInfo> into IEnumerable<QuantityInfo>
- changed the Quantity.Infos from IReadOnlyCollection to IReadOnlyList
@lipchev lipchev requested a review from angularsen February 7, 2025 17:09
@lipchev
Copy link
Collaborator Author

lipchev commented Feb 7, 2025

@angularsen I think this is ready, I don't have anything more to add for the time being.

There is of course a Part II and Part III of this- my other project is still several times faster on these... 🚀

@angularsen
Copy link
Owner

Oh boy, I'll need some time to just read through this 😄

@lipchev lipchev mentioned this pull request Apr 2, 2025
24 tasks
@lipchev
Copy link
Collaborator Author

lipchev commented Apr 2, 2025

Oh boy, I'll need some time to just read through this 😄

Don't be deterred, it's mostly just benchmark results (and the TLDR is that the numbers are better).

By the way, I don't know if you're also having this issue with Rider:

https://youtrack.jetbrains.com/issue/RSRP-499956/Local-Variable-Inline-Evaluation-Fails-for-System.Type

I've opened the ticket 2 months ago, and it doesn't seem to have gotten much traction - if you're seeing a similar issue please up-vote it (with this PR you may be seeing something like 'No unit information found for key..`).

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 think this is more or less ready to go, just a few minor suggestions and comments.

Unless you want a second review, you can just merge when you think it's ready.

}
// TODO this is certain to have terrible performance (especially on the first run)
// TODO we should consider adding a (lazy) dictionary for these
// Use case-sensitive match to reduce ambiguity.
Copy link
Owner

Choose a reason for hiding this comment

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

I don't have a good context here, but why can we skip case-insensitive matching here?

At least for parsing, I find it convenient that the library is case-insensitive as long as it is not ambiguous.

Copy link
Owner

@angularsen angularsen Apr 6, 2025

Choose a reason for hiding this comment

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

Ok I think I follow, we want UnitParser to contain the logic for case-insensitive-if-unique, and let UnitAbbreviationsCache be a more straight forward lookup with less logic?

I guess UnitParser could pass a case-sensitivity option to the lookup in UnitAbbreviationsCache, maybe.

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 don't have a good context here, but why can we skip case-insensitive matching here?

At least for parsing, I find it convenient that the library is case-insensitive as long as it is not ambiguous.

There are some units (between different quantities) which aren't ambiguous when compared in a case-sensitive manner (I did a test - and from memory there were around 20 units in total, for which this was helpful).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ok I think I follow, we want UnitParser to contain the logic for case-insensitive-if-unique, and let UnitAbbreviationsCache be a more straight forward lookup with less logic?

I guess UnitParser could pass a case-sensitivity option to the lookup in UnitAbbreviationsCache, maybe.

First part of the plan was to move everything to their domain of responsibility. This shouldn't be part of the UnitAbbreviationsCach, in my other project this is part of the UnitParser:

private List<(UnitInfo UnitInfo, string Abbreviation)> FindAllMatchingUnitsForCulture(string unitAbbreviation, CultureInfo culture,
    StringComparison comparison)

My long-term plan (haven't done it yet) was to create a Caching version of the UnitParser that is able to satisfy the previous method signature (likely this would be backed by a case-insensitive dictionary).

Currently every call to this function, is causing the UnitAbbreviations to be get fully-cached, so we might just as well save the mapped abbreviations.

Comment on lines -73 to +84
/// Quantity value type, such as <see cref="Length"/> or <see cref="Mass"/>.
/// Quantity value type, such as <see cref="Length" /> or <see cref="Mass" />.
/// </summary>
public Type ValueType { get; }
public Type QuantityType { get; }

/// <inheritdoc cref="QuantityType" />
[Obsolete("Replaced by the QuantityType property.")]
[DebuggerBrowsable(DebuggerBrowsableState.Never)]
public Type ValueType
{
get => QuantityType;
}
Copy link
Owner

Choose a reason for hiding this comment

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

Agreed, but why not just remove it since this is targeting v6?
We could deprecate in v5 instead and offer a similar change there.

Comment on lines +427 to +459
[Obsolete("Methods accepting a culture name are deprecated in favor of using an instance of the IFormatProvider.")]
public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, string? culture)
{
if (!TryGetUnitType(quantityName, out Type? unitType))
throw new UnitNotFoundException($"The unit type for the given quantity was not found: {quantityName}");

var cultureInfo = CultureHelper.GetCultureOrInvariant(culture);

var fromUnit = UnitsNetSetup.Default.UnitParser.Parse(fromUnitAbbrev, unitType, cultureInfo); // ex: ("m", LengthUnit) => LengthUnit.Meter
var fromQuantity = Quantity.From(fromValue, fromUnit);
return ConvertByAbbreviation(fromValue, quantityName, fromUnitAbbrev, toUnitAbbrev, CultureHelper.GetCultureOrInvariant(culture));
}

var toUnit = UnitsNetSetup.Default.UnitParser.Parse(toUnitAbbrev, unitType, cultureInfo); // ex:("cm", LengthUnit) => LengthUnit.Centimeter
return fromQuantity.As(toUnit);
/// <summary>
/// Convert between any two quantity units by their abbreviations, such as converting a "Length" of N "m" to "cm".
/// This is particularly useful for creating things like a generated unit conversion UI,
/// where you list some selectors:
/// a) Quantity: Length, Mass, Force etc.
/// b) From unit: Meter, Centimeter etc if Length is selected
/// c) To unit: Meter, Centimeter etc if Length is selected
/// </summary>
/// <param name="fromValue">
/// Input value, which together with <paramref name="fromUnitAbbrev" /> represents the quantity to
/// convert from.
/// </param>
/// <param name="quantityName">The invariant quantity name, such as "Length". Does not support localization.</param>
/// <param name="fromUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
/// <param name="toUnitAbbrev">The abbreviation of the unit in the given culture, such as "m".</param>
/// <param name="formatProvider">
/// The format provider to use for lookup. Defaults to <see cref="System.Globalization.CultureInfo.CurrentCulture" />
/// if null.
/// </param>
/// <example>double centimeters = ConvertByName(5, "Length", "m", "cm"); // 500</example>
/// <returns>Output value as the result of converting to <paramref name="toUnitAbbrev" />.</returns>
/// <exception cref="QuantityNotFoundException">
/// Thrown when no quantity information is found for the specified quantity name.
/// </exception>
/// <exception cref="UnitNotFoundException">No units match the abbreviation.</exception>
/// <exception cref="AmbiguousUnitParseException">More than one unit matches the abbreviation.</exception>
public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, IFormatProvider? formatProvider)
Copy link
Owner

Choose a reason for hiding this comment

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

I was about to comment on the same thing reading the above obsoletion of string-based culture names.

I'm not sure how I feel about string vs IFormatProvider, since we don't really use the culture formatting for anything, we just rely on the culture name.

If we end up just getting cached CultureInfo instances via CultureInfo.GetCultureInfo(name) from these strings anyway, I guess we could argue to pass in those instance to begin with.
And yes, it makes more sense to pass in CultureInfo here rather than IFormatProvider + safe-casting.

Just to mention, ResourceManager has some built-in logic for locale fallback: https://learn.microsoft.com/en-us/globalization/locale/fallback

/// This key is particularly useful when using an enum-based unit in a hash-based collection,
/// as it avoids the boxing that would normally occur when casting the enum to <see cref="Enum" />.
/// </remarks>
public virtual UnitKey UnitKey => Value;
Copy link
Owner

Choose a reason for hiding this comment

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

I like the new UnitKey.

@lipchev
Copy link
Collaborator Author

lipchev commented Apr 6, 2025

Agreed, but why not just remove it since this is targeting v6?
We could deprecate in v5 instead and offer a similar change there.

I was hoping you would say that (regarding the QuantityInfo.ValueType)..

@lipchev
Copy link
Collaborator Author

lipchev commented Apr 6, 2025

public static double ConvertByAbbreviation(double fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, IFormatProvider? formatProvider)
I'm not sure how I feel about string vs IFormatProvider, since we don't really use the culture formatting for anything, we just rely on the culture name.

In my latest version this has the following signature:

public static QuantityValue ConvertByAbbreviation(QuantityValue fromValue, string quantityName, string fromUnitAbbrev, string toUnitAbbrev, CultureInfo? culture)

while the other overloads are marked as [Obsolete] , however as with the other stuff- we could introduce the change in v5 and remove the [Obsolete] stuff in v6, but that's something that can be done at any time (before the release of v6).

@lipchev
Copy link
Collaborator Author

lipchev commented Apr 6, 2025

Just to mention, ResourceManager has some built-in logic for locale fallback: https://learn.microsoft.com/en-us/globalization/locale/fallback

Yes, I think we're actually doing this all wrong (in terms of performance)- there is a huge benefit for re-using the ResourceManager, in my latest iteration, this is actually a parameter given to the QuantityInfo:

    /// <summary>
    ///     Initializes a new instance of the <see cref="QuantityInfo" /> class.
    /// </summary>
    /// <param name="name">The name of the quantity.</param>
    /// <param name="quantityType">The type representing the quantity.</param>
    /// <param name="baseDimensions">The base dimensions of the quantity.</param>
    /// <param name="unitAbbreviations">
    ///     When provided, the resource manager used for localizing the quantity's unit abbreviations.
    /// </param>
    /// <exception cref="ArgumentNullException">
    ///     Thrown if <paramref name="name" />, <paramref name="quantityType" />, or <paramref name="baseDimensions" /> is
    ///     <c>null</c>.
    /// </exception>
    protected QuantityInfo(string name, Type quantityType, BaseDimensions baseDimensions, ResourceManager? unitAbbreviations)
    {
        Name = name ?? throw new ArgumentNullException(nameof(name));
        QuantityType = quantityType ?? throw new ArgumentNullException(nameof(quantityType));
        BaseDimensions = baseDimensions ?? throw new ArgumentNullException(nameof(baseDimensions));
        UnitAbbreviations = unitAbbreviations;
    }

At the same time, this allows us to actually use custom resource dictionaries, defined by the client-app (both for customizing the existing stuff, as well as the HowMuch):

        public static readonly QuantityInfo<HowMuch, HowMuchUnit> Info = new(
            HowMuchUnit.Some,
            new UnitDefinition<HowMuchUnit>[]
            {
                new(HowMuchUnit.Some, "Some", BaseUnits.Undefined),
                new(HowMuchUnit.ATon, "Tons", new BaseUnits(mass: MassUnit.Tonne), QuantityValue.FromTerms(1, 10)),
                new(HowMuchUnit.AShitTon, "ShitTons", BaseUnits.Undefined, QuantityValue.FromTerms(1, 100))
            },
            new BaseDimensions(0, 1, 0, 0, 0, 0, 0),
            // providing a resource manager for the unit abbreviations (optional)
            Properties.CustomQuantities_HowMuch.ResourceManager); 

@lipchev
Copy link
Collaborator Author

lipchev commented Apr 6, 2025

@angularsen Did you notice this issue? I'm pretty sure that this should also appear in v5 as Inspecting the state of an object of type System.Type.. is not supported (or something similar).

By the way, I don't know if you're also having this issue with Rider:

https://youtrack.jetbrains.com/issue/RSRP-499956/Local-Variable-Inline-Evaluation-Fails-for-System.Type

I've opened the ticket 2 months ago, and it doesn't seem to have gotten much traction - if you're seeing a similar issue please up-vote it (with this PR you may be seeing something like 'No unit information found for key..`).

@angularsen
Copy link
Owner

Did you notice this issue?

No, never seen that one in Rider. I haven't tried the example given in the issue though.

@lipchev lipchev merged commit b2e8dd9 into angularsen:release/v6 Apr 11, 2025
1 check passed
@lipchev
Copy link
Collaborator Author

lipchev commented Apr 11, 2025

@angularsen I'm merging this, so that it's not in the way.. My plan was to close #1463 next, by copy-pasting the tests from my other project (where the property is removed), leaving us just code changes to review- hopefully there won't be any differences in the tests, other than the removal of the property from the types that support it (99%).

@angularsen
Copy link
Owner

Alright!

@lipchev
Copy link
Collaborator Author

lipchev commented Apr 11, 2025

I see that the code coverage report is not happy, but no worries- here's what I've got from my latest build:

  • Uncovered lines: 585
  • Coverable lines: 26691
  • Total lines: 154246
  • Line coverage: 97.8%

(with most of it being in the UnitsNet.Debug namespace)

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.

2 participants