0

I have a general enum, let's say G that has some flagged values (One = 0 / Two = 1 / Three = 2 / Four = 4 / Five = 8, and so on).

I then have another enum (let's say B) that "extends" G with this kind of pattern : One = G.One / Two = G.Two / Three = G.Three / Four = G.Four (and that's all, no Five in this one).

I finally have have a last enum (let's say C) that also "extends" G with the same pattern type but other values : Three = G.Three / Four = G.Four / Five = G.Five (no One and Two in this one).

I'd like to find a generic function to convert B into C or C into B. For example, if I have "A valsAsA = A.One | A.Three | A.Four", I'd like a function like this : "B valsAsB = convert(valsAsA);" that would gives me "B.Three | A.Four".

This should be really generic because I have not only A and B enums, but also C, D, E... with different possible enum values, but always values from the generic enum.

Is it possible without checking all possibilities and adapting the function each time I add a new enum ?

An example:

    public enum General : int
    {
        One = 0,
        Two = 1,
        Three = 2,
        Four = 4,
        Five = 8
    }

    public enum A : int
    {
        One = General.One,
        Two = General.Two,
        Three = General.Three,
        Four = General.Four,
    }

    public enum B : int
    {
        Three = General.Three,
        Four = General.Four,
        Five = General.Five
    }

    public enum C : int
    {
        One = General.One,
        Three = General.Three,
        Five = General.Five
    }

    public class Test
    {
        public void testConvert()
        {
            A valAsA = A.One | A.Three | A.Four;
            B valAsB = convertFct(valAsA); // Should give me "B.Three | B.Four"
            C valAsC = convertFct(valAsA); // Should give me "C.One | C.Three"
        }
    }

I tested that :

A valAsA = A.One | A.Three | A.Four; 
C valAsC = (C)valAsA;
C valAsCReal = C.One | C.Three; // expected result

with no luck.. valAsC = 6 while valAsCReal = 2...

Thank you very much

PP_RhuM
  • 81
  • 8
  • Note: should be good to have only one function with a generic return has i'd like not to create a new function each time I add a new enum. – PP_RhuM Jul 01 '19 at 11:35
  • This sounds like a [xy-problem](http://xyproblem.info/). What do you want to achieve with that? You can easily cast from one enum to another like `valsAsB = (B)valsAsA;` – René Vogt Jul 01 '19 at 11:37
  • You know that there is no such thing as enum inheritance and `A valsAsA = (A)1025;` is still a valid `A` value, right? – vasily.sib Jul 01 '19 at 11:38
  • 2
    Depending on your scenario, you may be doing things here with enums that are better served with `BitArray` and masks (or less geekily, a `HashSet` of more meaningful values that you can union and intersect). Enums tend to be overused especially in "dynamic" scenarios, where what you've got isn't actually a statically safe-at-compile-time enum, which is more or less the intended use. – Jeroen Mostert Jul 01 '19 at 11:40
  • See an example in my "answer" – PP_RhuM Jul 01 '19 at 11:42
  • @JeroenMostert , The application I'm working on is as it, I can't change everything... :/ – PP_RhuM Jul 01 '19 at 11:44
  • they are all the same (literally). Check this: `var a = A.One | A.Two; var r = a.HasFlag(StringSplitOptions.RemoveEmptyEntries);` – vasily.sib Jul 01 '19 at 11:47
  • I tested that : ``` A valAsA = A.One | A.Three | A.Four; C valAsC = (C)valAsA; C valAsCReal = C.One | C.Three; ``` with no chance.. valAsC = 6 while valAsCReal = 2... The HasFlag function returns a boolean and don't take this kind of argument.. – PP_RhuM Jul 01 '19 at 12:41
  • @PP_RhuM Take a look at [Enum.ToObject](https://learn.microsoft.com/en-us/previous-versions/windows/silverlight/dotnet-windows-silverlight/k6x6b517(v%3Dvs.95)). – Alessandro D'Andria Jul 01 '19 at 14:41
  • Thank you @AlessandroD'Andria , but not working as expected. Enum.ToObject(typeof(C), valAsA) == 6 but 2 or "C.Three" is expected in my example... :'( – PP_RhuM Jul 01 '19 at 14:45
  • Don't understand what you're trying to do here, you explicitly set `valAsA` to *three* flags, but `valAsCReal` only to *two* of them, of course they're going to have different values. You're missing the extra 4 from `A.Four` (or the equivalent in `C` which you don't have). You need to clarify *why* you expected them to be equal. – Lasse V. Karlsen Jul 02 '19 at 08:44
  • valsAsA = 0 | 2 | 4 = 6. valsAsC doesn't have the "4" bit flag (Four), so converting valAsA to a C enum should gives me 0 | 2 = 2, but when casting, it gives me 6... anyway, I could get a solution (see answers). – PP_RhuM Jul 04 '19 at 14:29

2 Answers2

2

Doing this with generics is a little tricky because it is not possible to set up a type constraint that allows enumerations in general (see this question). The best you can do is constraint to struct, IConvertible and do a runtime check, as I do in this example.

If you can cope with that part of the ugliness, the rest is fairly simple:

First, write two methods to convert to and from a General. Since your enums are bit masks, the "conversion" is actually just a binary and operation against the sum of all possible values, which you can obtain using GetValues.

Once you have performed the and operation, you can return an enum of the appropriate type by converting the integer using Enum.ToObject().

static public class ExtensionMethods
{
    static public General ToGeneral<T>(this T input) where T : struct, IConvertible
    {
        if (!typeof(T).IsEnum) throw new ArgumentException("Input must be an enum.");

        return (General)((int)(object)input & Enum.GetValues(typeof(General)).Cast<int>().Sum());
    }

    static public T ToEnum<T>(this General input)
    {
        if (!typeof(T).IsEnum) throw new ArgumentException("Output type must be an enum.");

        return (T)Enum.ToObject(typeof(T), (int)input & Enum.GetValues(typeof(T)).Cast<int>().Sum());
    }
}

Once those are written, conversion to and from any enum is easy:

static public TOut Convert<TIn,TOut>(TIn input) where TIn : struct, IConvertible where TOut: struct, IConvertible
{
    var general = input.ToGeneral();
    return general.ToEnum<TOut>();
}

Test code:

public static void Main()
{
    A valAsA = A.One | A.Three | A.Four;
    B valAsB = Convert<A, B>(valAsA);  // Should give me "B.Three | B.Four"
    C valAsC = Convert<A, C>(valAsA); // Should give me "C.One | C.Three"

    Console.WriteLine("{0} should equal {1}", valAsB, (B.Three | B.Four));
    Console.WriteLine("{0} should equal {1}", valAsC, (C.One | C.Three));
}

Output:

6 should equal 6
Three should equal Three

See the code in action at DotNetFiddle

John Wu
  • 50,556
  • 8
  • 44
  • 80
  • Really elegant solution !! thank you very much. Using my code, I don't need to do the "ToGeneral" function because I'm sure that I can cast A, B or C to General simply using "(General)valAsA" for example and I have in fact many "General" enums (real name: MassOptions / PowerOptions / ThermalOptions ...) so I'd need to create a function for all these. I will adapt my own solution with Linq as you did ;) – PP_RhuM Jul 02 '19 at 08:12
0

Damn !!! I could create this incredible function that answer to my question but please... tell me that there is something more elegant... xD

        private TRet ConvertIt<TRet, TOrig>(TOrig values) where TOrig : struct, IConvertible where TRet : struct, IConvertible
        {
            if (!typeof(TOrig).IsEnum || 
                !typeof(TRet).IsEnum ||
                !typeof(int).IsAssignableFrom(typeof(TOrig)) || 
                !typeof(int).IsAssignableFrom(typeof(TRet)))
            {
                throw new ArgumentException("TOrig and TRet must be an enumerated type extending integer");
            }

            bool retEnumHasZero = false;

            foreach (var flag in Enum.GetValues(typeof(TRet)))
            {
                if ((int)flag == 0)
                {
                    retEnumHasZero = true;
                    break;
                }
            }

            if (!retEnumHasZero)
            {
                throw new ArgumentException("TRet enum must have the 0 flag");
            }

            Dictionary<int, Enum> valsOrig = new Dictionary<int, Enum>();

            foreach (var flag in Enum.GetValues(typeof(TOrig)))
            {
                valsOrig.Add((int)flag, (Enum)flag);
            }

            object valuesAsObject = values;
            var valuesAsEnum = (Enum)valuesAsObject;


            int returnedValue = 0;

            foreach (var flag in Enum.GetValues(typeof(TRet)))
            {
                int flagAsInt = (int)flag;

                if (valsOrig.ContainsKey(flagAsInt) && valuesAsEnum.HasFlag(valsOrig[flagAsInt]))
                {
                    returnedValue |= flagAsInt;
                }
            }

            return (TRet)Enum.ToObject(typeof(TRet), returnedValue);
        }

Using the function :

A valAsA = A.One | A.Two | A.Three | A.Four;
C valAsC = ConvertIt<C, A>(valAsA);

Edit : This implementation looks better :

        private T ConvertIt<T>(Enum values) where T : struct, IConvertible
        {
            if (!typeof(T).IsEnum)
            {
                throw new ArgumentException("Type to return must be an enumerated type");
            }

            if (!Enum.IsDefined(typeof(T), 0))
            {
                throw new ArgumentException("Type to return enum must have the 0 flag");
            }

            int returnedValue = 0;

            foreach (var flag in Enum.GetValues(values.GetType()))
            {
                int flagAsInt = (int)flag;

                if (values.HasFlag((Enum)flag) && Enum.IsDefined(typeof(T), flagAsInt))
                {
                    returnedValue |= flagAsInt;
                }
            }

            return (T)Enum.ToObject(typeof(T), returnedValue);
        }

Using the function :

A valAsA = A.One | A.Two | A.Three | A.Four;
C valAsC = ConvertIt<C>(valAsA);

Finally, with the help of John Wu, here is the final function =>

        private T ConvertIt<T>(Enum input) where T : struct, IConvertible
        {
            if (!typeof(T).IsEnum)
            {
                throw new ArgumentException("Type to return must be an enumerated type");
            }

            return (T)Enum.ToObject(typeof(T), (int)(object)input & Enum.GetValues(typeof(T)).Cast<int>().Sum());
        }
PP_RhuM
  • 81
  • 8