1

I'm trying to create an extension method to do this:

enum AlphaBet { A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z }

IEnumerable<AlphaBet> rangeCtoG = AlphaBet.C.RangeToInc(AlphaBet.G);

But this won't compile (as TEnum is generic):

public static IEnumerable<TEnum> RangeToInc<TEnum>(this TEnum from, TEnum to)
    where TEnum : struct, IComparable, IFormattable, IConvertible //
{
    for (; from <= to; from++)
        yield return from;
}

So I turned to expressions:

public delegate void MyFunc<T>(ref T arg); // allow PostIncrementAssign to change the input parameter

public static IEnumerable<TEnum> RangeToInc<TEnum>(this TEnum from, TEnum to)
    where TEnum : struct, IComparable, IFormattable, IConvertible //
{
    var fromRefParamExpr = Expression.Parameter(typeof(TEnum).MakeByRefType(), "fromParam");
    var incrementExpr = Expression.PostIncrementAssign(fromRefParamExpr);
    var increment = Expression.Lambda<MyFunc<TEnum>>(incrementExpr, fromRefParamExpr).Compile();

    var fromParamExpr = Expression.Parameter(typeof(TEnum), "fromParam");
    var toParamExpr = Expression.Parameter(typeof(TEnum), "toParam");
    var lessThanOrEqualExpr = Expression.LessThanOrEqual(fromParamExpr, toParamExpr);
    var lessThanOrEqual = Expression.Lambda<Func<TEnum, TEnum, bool>>(
        lessThanOrEqualExpr, toParamExpr, fromParamExpr).Compile();

    for (; lessThanOrEqual(to, from); increment(ref from))
        yield return from;
}

It works great with integers, but not with enums: the line Expression.PostIncrementAssign(fromRefParamExpr) fails with exception:

System.InvalidOperationException:
    'The unary operator PostIncrementAssign is not defined for the type
    '...+AlphaBet'.'

Which is quite suprising - isn't an enum a numeric type? what am I supposed to do? cast back and forth enumint?

Tar
  • 8,529
  • 9
  • 56
  • 127
  • What is `Fun` in your code? – Evk Jun 01 '17 at 13:47
  • @Evk, sorry, added it now. It's a delegate that has a `ref` parameter to allow `PostIncrementAssign` alter the parameter. – Tar Jun 01 '17 at 13:51
  • 1
    The most fascinating thing is that the generated IL code would be equivalent... See [this](https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgBgATIIwG4CwAUMgMzoCmAdgK4C26Aaoes+gN5Muf1boC86qKB07N6AJj7osw9AF9CJTBIDC6QuwKdF9dAFksACh0A3AJRrNLDSJbH48fJZvIA7OmOPO8p5lJgKwHpiBv6BZhac1jbu9p7Rru5xzN6yQA=)... And yes, what you need is to *cast back and forth enum ⇔ int*? – xanatos Jun 01 '17 at 13:53
  • And you are fine that it won't work with enums that have gaps (like `enum Test {A = 1, B = 3}`? – Evk Jun 01 '17 at 14:02
  • 1
    @Tar Because enum's are numeric, when you call `Expression.PostIncrementAssign` [there is a check](https://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Ast/UnaryExpression.cs,984) in`IsArithmetic` method `if (!type.IsEnum)` – Dudi Keleti Jun 01 '17 at 14:04
  • @Evk, yes, that's ok – Tar Jun 01 '17 at 14:11
  • @DudiKeleti, what can I do about it? is there a way around it? I could've called `new UnaryExpression(...)` but it's private... – Tar Jun 01 '17 at 14:14
  • Are you looking for other solutions? Because you can do that with `dynamic` quite easily. – Evk Jun 01 '17 at 14:31
  • @Tar If it wasn't `enum` you can do operators overloading and then `GetUserDefinedUnaryOperatorOrThrow` [method](http://referencesource.microsoft.com/#System.Core/Microsoft/Scripting/Ast/UnaryExpression.cs,364) should work, but you can't do that for enum :/ maybe you need to think on other solution. – Dudi Keleti Jun 01 '17 at 14:34

2 Answers2

2

maybe this is can help you..

    public static IEnumerable<TEnum> RangeToInc<TEnum>(this TEnum from, TEnum to) where 
        TEnum : struct, IComparable, IFormattable, IConvertible
    {
        return Enum.GetValues(typeof(TEnum)).Cast<TEnum>().Where(
            a => a.CompareTo(from) >= 0 
                && a.CompareTo(to) <= 0).ToArray(); //Actually it does not need ToArray


    }

Slightly more expandable

    public static IEnumerable<TEnum> RangeToInc<TEnum>(this TEnum from, TEnum to) where
        TEnum : struct, IComparable, IFormattable, IConvertible
    {
        if (typeof(TEnum).IsEnum)
            return Enum.GetValues(typeof(TEnum)).Cast<TEnum>().Where(
                a => a.CompareTo(from) >= 0
                    && a.CompareTo(to) <= 0);
        else if (typeof(TEnum).IsNumericType())
        {
            int start = Convert.ToInt32(from);
            int count = Convert.ToInt32(to) - start;
            return Enumerable.Range(start, count + 1).Cast<TEnum>();
        }

        throw new NotImplementedException();
    }
    public static bool IsNumericType(this Type type)
    {
        switch (Type.GetTypeCode(type))
        {
            case TypeCode.Byte:
            case TypeCode.SByte:
            case TypeCode.UInt16:
            case TypeCode.UInt32:
            case TypeCode.UInt64:
            case TypeCode.Int16:
            case TypeCode.Int32:
            case TypeCode.Int64:
            case TypeCode.Decimal:
            case TypeCode.Double:
            case TypeCode.Single:
                return true;
            default:
                return false;
        }
    }

using

        IEnumerable<DayOfWeek> enumRange = DayOfWeek.Sunday.RangeToInc(DayOfWeek.Wednesday);
        foreach (var item in enumRange)
        {
            Console.Write(item + " ");
            //
        }
        Console.WriteLine();
        int a = 4;
        var intRange = a.RangeToInc(10);
        foreach (var item in intRange)
        {
            Console.Write(item + " ");
        }
        //output 
        // Sunday Monday Tuesday Wednesday
        // 4 5 6 7 8 9 10
levent
  • 3,464
  • 1
  • 12
  • 22
1

Sadly you'll have to cast back and forth... What you can do is cache the resulting expression tree and use the internal .NET runtime for this:

public static class EnumerableHelper
{
    public static IEnumerable<TEnum> RangeToInc<TEnum>(this TEnum from, TEnum to)
        where TEnum : struct, IComparable, IFormattable, IConvertible
    {
        var cmp = Comparer<TEnum>.Default;

        while (cmp.Compare(from, to) <= 0)
        {
            yield return from;
            from = EnumerableHelperImpl<TEnum>.Increment(from);
        }
    }

    private static class EnumerableHelperImpl<TEnum>
    {
        public static readonly Func<TEnum, TEnum> Increment;

        static EnumerableHelperImpl()
        {
            var par = Expression.Parameter(typeof(TEnum));

            Expression body = typeof(TEnum).IsEnum ?
                Expression.Convert(Expression.Increment(Expression.Convert(par, Enum.GetUnderlyingType(typeof(TEnum)))), typeof(TEnum)) :
                Expression.Increment(par);

            Increment = Expression.Lambda<Func<TEnum, TEnum>>(body, par).Compile();
        }
    }
}

and then:

IEnumerable<AlphaBet> rangeCtoG = AlphaBet.C.RangeToInc(AlphaBet.G);
IEnumerable<int> range2 = 5.RangeToInc(8);

Note that you can even create ranges betweeen int and not only between enum. The "trick" here is to use a private EnumerableHelperImpl<TEnum>, that will be different depending on the TEnum. The .NET will generate those EnumerableHelperImpl<TEnum> based on the TEnum types used and will cache them. The code will work even with non-int enums.

xanatos
  • 109,618
  • 12
  • 197
  • 280