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

Problem with ENUMS when UnitAbbreviationsCache.Default.GetDefaultAbbreviation is use. #1301

Closed
farenasmz opened this issue Aug 10, 2023 · 4 comments · Fixed by #1302
Closed
Labels

Comments

@farenasmz
Copy link

Hello everyone,

I'm currently working on a project utilizing units.net and have encountered an issue while handling custom units. I've defined a new custom unit called GasFlowRate and created two specific measures within it: StandardCubicMetersPerDay and ThousandStandardCubicFeetPerDay.

I've used UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation to set the default abbreviations for these units, and they seem to be mapped correctly.

However, when I attempt to retrieve the abbreviation for these units using UnitAbbreviationsCache.Default.GetDefaultAbbreviation(unit);, I encounter the following error: InnerException: null, Message: "No abbreviation is specified for Enum.ThousandStandardCubicFeetPerDay".

Interestingly, if I specify the unit directly, like UnitAbbreviationsCache.Default.GetDefaultAbbreviation(GasFlowRateUnit.StandardCubicMetersPerDay), it retrieves the abbreviation without any issue.

But when I send the unit as a parameter to a function, e.g., GetAbbreviation(quantity, GasFlowRateUnit.StandardCubicMetersPerDay), it throws the aforementioned error.

I've made sure that the unit is being passed correctly, and the enumeration value seems to be in the right format. I've attempted to cast the enum to its specific type and used various methods to handle it, but the issue persists.

Here's a snippet of the relevant code:

Is there something specific I need to be aware of when working with custom units and abbreviations in units.net? Any insights or suggestions on how to resolve this issue would be greatly appreciated.

Thank you in advance for your assistance!

public static string GetAbbreviation(this IQuantity quantity, Enum unit)
        {
            var unitType = quantity.QuantityInfo.UnitType;
            var unitValue = Convert.ToInt32(unit);
            var result = UnitAbbreviationsCache.Default.GetDefaultAbbreviation(unit);
            return result;
       }            
 public enum GasFlowRateUnit
    {
        StandardCubicMetersPerDay = 1,
        ThousandStandardCubicFeetPerDay = 2,
    }

  public struct GasFlowRate : IQuantity
    {
        public GasFlowRate(double value, GasFlowRateUnit unit)
        {
            Unit = unit;
            Value = value;
            AddAbbreviations();
        }

        private void AddAbbreviations()
        {
            UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(GasFlowRateUnit.StandardCubicMetersPerDay, "m³/d");
            UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(GasFlowRateUnit.ThousandStandardCubicFeetPerDay, "kft³/d");
        }

        Enum IQuantity.Unit => Unit;

        public GasFlowRateUnit Unit { get; }

        public double Value { get; }

        #region IQuantity

        private static readonly GasFlowRate Zero = new GasFlowRate(0, GasFlowRateUnit.StandardCubicMetersPerDay);

        public BaseDimensions Dimensions => BaseDimensions.Dimensionless;

        public QuantityType Type => QuantityType.Undefined;

        public QuantityInfo QuantityInfo => new QuantityInfo(nameof(GasFlowRate), Type.GetType(),
        new UnitInfo[]
              {
                new UnitInfo<GasFlowRateUnit>(GasFlowRateUnit.StandardCubicMetersPerDay, "m³/d", BaseUnits.Undefined),
                new UnitInfo<GasFlowRateUnit>(GasFlowRateUnit.ThousandStandardCubicFeetPerDay, "kft³/d", BaseUnits.Undefined),
              }, GasFlowRateUnit.StandardCubicMetersPerDay, Zero, BaseDimensions.Dimensionless);

        public double As(Enum unit)
        {
            if (Unit == GasFlowRateUnit.StandardCubicMetersPerDay && unit.Equals(GasFlowRateUnit.ThousandStandardCubicFeetPerDay))
            {
                return Value * 35.315 / 1000;
            }

            if (Unit == GasFlowRateUnit.ThousandStandardCubicFeetPerDay && unit.Equals(GasFlowRateUnit.StandardCubicMetersPerDay))
            {
                return Value * 1000 / 35.315;
            }

            return Convert.ToDouble(unit);
        }

        public double As(UnitSystem unitSystem) => throw new NotImplementedException();

        public IQuantity ToUnit(Enum unit)
        {
            if (unit is GasFlowRateUnit gasRateFlowUnit)
            {
                return new GasFlowRate(As(unit), gasRateFlowUnit);
            }

            throw new ArgumentException("Must be of type GasFlowRateUnit.", nameof(unit));
        }

        public IQuantity ToUnit(UnitSystem unitSystem) => throw new NotImplementedException();

        public static GasFlowRate FromStandardCubicMetersPerDay(QuantityValue footPerDay)
        {
            double value = (double)footPerDay;
            return new GasFlowRate(value, GasFlowRateUnit.StandardCubicMetersPerDay);
        }

        public override string ToString() => $"{Value} {UnitAbbreviationsCache.Default.GetDefaultAbbreviation(Unit)}";

        public string ToString(string format, IFormatProvider formatProvider) => $"{Value} {UnitAbbreviationsCache.Default.GetDefaultAbbreviation(Unit)} ({format}, {formatProvider})";

        public string ToString(IFormatProvider provider) => $"{Value} {UnitAbbreviationsCache.Default.GetDefaultAbbreviation(Unit)} ({provider})";

        public string ToString(IFormatProvider provider, int significantDigitsAfterRadix) => $"{Value} {UnitAbbreviationsCache.Default.GetDefaultAbbreviation(Unit)} ({provider}, {significantDigitsAfterRadix})";

        public string ToString(IFormatProvider provider, string format, params object[] args) => $"{Value} {UnitAbbreviationsCache.Default.GetDefaultAbbreviation(Unit)} ({provider}, {string.Join(", ", args)})";

        public bool Equals(IQuantity other, IQuantity tolerance)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
@farenasmz farenasmz added the bug label Aug 10, 2023
angularsen added a commit that referenced this issue Aug 10, 2023
Fixes #1301

When calling methods to look up unit abbreviation for a unit enum value,
which is cast to Enum either by variable or method parameter,
then the actual enum type was lost and it failed to get its enum name.

```
System.ArgumentException: Type provided must be an Enum.
   at System.Enum.GetEnumInfo(RuntimeType enumType, Boolean getNames)
   at System.RuntimeType.GetEnumName(Object value)
   at UnitsNet.UnitAbbreviationsCache.TryGetUnitAbbreviations(Type unitType, Int32 unitValue, IFormatProvider formatProvider, String[]& abbreviations) in C:\dev\unitsnet\UnitsNet\CustomCode\UnitAbbreviationsCache.cs:line 246
```

### Changes
- Handle edge-case when `Enum` type is passed instead of an actual unit enum type
@angularsen
Copy link
Owner

Hi, what version are you on? I see you are using QuantityType and it was removed in v5.
I updated your code to v5, but it still fails.

I believe it boils down to casting to Enum and the lookup fails as shown in CastToEnum_Fails(), but GenericEnum_Ok() passes.

using System;
using Xunit;

namespace UnitsNet.Tests;

public class GasFlowTests
{
    public GasFlowTests()
    {
         UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(GasFlowRateUnit.StandardCubicMetersPerDay, "m³/d");
         UnitAbbreviationsCache.Default.MapUnitToDefaultAbbreviation(GasFlowRateUnit.ThousandStandardCubicFeetPerDay, "kft³/d");
    }

    [Fact]
    public void GenericEnum_Ok()
    {
        Assert.Equal("m³/d", UnitAbbreviationsCache.Default.GetDefaultAbbreviation(GasFlowRateUnit.StandardCubicMetersPerDay));
    }

    [Fact]
    public void CastToEnum_Fails()
    {
        Assert.Equal("m³/d", UnitAbbreviationsCache.Default.GetDefaultAbbreviation((Enum)GasFlowRateUnit.StandardCubicMetersPerDay));
    }

    public enum GasFlowRateUnit
    {
        StandardCubicMetersPerDay = 1,
        ThousandStandardCubicFeetPerDay = 2,
    }
}
System.ArgumentException: Type provided must be an Enum.

System.ArgumentException
Type provided must be an Enum.
   at System.Enum.GetEnumInfo(RuntimeType enumType, Boolean getNames)
   at System.RuntimeType.GetEnumName(Object value)
   at UnitsNet.UnitAbbreviationsCache.TryGetUnitAbbreviations(Type unitType, Int32 unitValue, IFormatProvider formatProvider, String[]& abbreviations) in C:\dev\unitsnet\UnitsNet\CustomCode\UnitAbbreviationsCache.cs:line 246
   at UnitsNet.UnitAbbreviationsCache.GetUnitAbbreviations(Type unitType, Int32 unitValue, IFormatProvider formatProvider) in C:\dev\unitsnet\UnitsNet\CustomCode\UnitAbbreviationsCache.cs:line 231
   at UnitsNet.UnitAbbreviationsCache.GetDefaultAbbreviation(Type unitType, Int32 unitValue, IFormatProvider formatProvider) in C:\dev\unitsnet\UnitsNet\CustomCode\UnitAbbreviationsCache.cs:line 204
   at UnitsNet.UnitAbbreviationsCache.GetDefaultAbbreviation[TUnitType](TUnitType unit, IFormatProvider formatProvider) in C:\dev\unitsnet\UnitsNet\CustomCode\UnitAbbreviationsCache.cs:line 190
   at UnitsNet.Tests.GasFlowTests.CastToEnum() in C:\dev\unitsnet\UnitsNet.Tests\GasFlowTests.cs:line 26
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)

I took a quick look and found that the lookup implementation incorrectly tries to do typeof() on the generic type, but when passing Enum that type information is lost.

A fix is on the way in #1302.

angularsen added a commit that referenced this issue Aug 10, 2023
Fixes #1301

When looking up unit abbreviation for a unit enum value that is cast to `Enum`, for example via variable or method parameter, the lookup failed due to using `typeof(TUnitEnum)` in the generic method. `Enum` satisfies the generic constraint, but the generic type no longer describes the original unit enum type. Instead, we must use `unitEnumValue.GetType()`.

```
System.ArgumentException: Type provided must be an Enum.
   at System.Enum.GetEnumInfo(RuntimeType enumType, Boolean getNames)
   at System.RuntimeType.GetEnumName(Object value)
   at UnitsNet.UnitAbbreviationsCache.TryGetUnitAbbreviations(Type unitType, Int32 unitValue, IFormatProvider formatProvider, String[]& abbreviations) in C:\dev\unitsnet\UnitsNet\CustomCode\UnitAbbreviationsCache.cs:line 246
```

### Changes
- Handle edge-case when `Enum` type is passed instead of an actual unit enum type
@angularsen
Copy link
Owner

Merged and should be out as nuget shortly.

Release UnitsNet/5.30.0 · angularsen/UnitsNet

@farenasmz
Copy link
Author

farenasmz commented Sep 5, 2023

Hi @angularsen! thanks for your answer I'm using 4.132.0

@angularsen
Copy link
Owner

@farenasmz are you able to try the latest v5 nuget and see if that helps? You may have to migrate some usages.

https://github.com/angularsen/UnitsNet/wiki/Upgrading-from-4.x-to-5.x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants