Skip to content

Instantly share code, notes, and snippets.

@cgbeutler
Last active October 21, 2022 02:05
Show Gist options
  • Save cgbeutler/699e96f4868a7ccdd004d1daf38407d9 to your computer and use it in GitHub Desktop.
Save cgbeutler/699e96f4868a7ccdd004d1daf38407d9 to your computer and use it in GitHub Desktop.
A set of useful enumeration extensions
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
namespace MonsterVial.Extensions
{
public static class EnumExt
{
/// <summary> Returns a value that is the combination of `self` and `flags`. Same as bitwise OR against `flags`. </summary>
public static T WithFlag<T>( this T self, T flags ) where T : struct, Enum => Enum<T>.AddFlags( self, flags );
/// <summary> Returns a value that is `self` with `flags` removed. Same as a bitwise AND against inverted `flags`. </summary>
public static T WithoutFlag<T>( this T self, T flags ) where T : struct, Enum => Enum<T>.SubtractFlags( self, flags );
/// <summary> Returns true and the next explicitly defined enum value up from the current one, if it exists. False otherwise. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static bool TryGetNext<T>( this T self, out T result ) where T : struct, Enum => Enum<T>.TryGetNext( self, out result );
/// <summary> Returns true and the next explicitly defined enum value down from the current one, if it exists. False otherwise. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static bool TryGetPrev<T>( this T self, out T result ) where T : struct, Enum => Enum<T>.TryGetPrev( self, out result );
/// <summary> Returns the next explicitly defined value up from self. Wraps to lowest value from largest value. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static T ShiftNext<T>( this T self ) where T : struct, Enum => Enum<T>.ShiftNext( self );
/// <summary> Returns the next explicitly defined value down from self. Wraps to highest value from lowest value. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static T ShiftPrev<T>( this T self ) where T : struct, Enum => Enum<T>.ShiftPrev( self );
};
public static class Enum<T>
where T : struct, Enum
{
/// <summary> True if the enum has the FlagsAttribute </summary>
public static readonly bool HasFlagsAttribute = typeof(T).GetCustomAttributes(typeof(FlagsAttribute), false).Length > 0;
/// <summary> An list of the enum values ordered as defined. Can contain duplicates. </summary>
public static readonly IReadOnlyList<T> Values = Enum.GetValues( typeof( T ) ).Cast<T>().ToList();
/// <summary> An ascending list of the enum values ordered by value. Duplicates removed. </summary>
public static readonly IReadOnlyList<T> ValuesAscending = Enum.GetValues( typeof( T ) ).Cast<T>().Distinct().OrderBy( v => v ).ToList();
/// <summary> An descending list of the enum values ordered by value. Duplicates removed. </summary>
public static readonly IReadOnlyList<T> ValuesDescending = Enum.GetValues( typeof( T ) ).Cast<T>().Distinct().OrderByDescending( v => v ).ToList();
/// <summary> An list of the enum value names ordered as defined. </summary>
public static readonly IReadOnlyList<string> Names = Enum.GetNames( typeof( T ) ).ToList();
/// <summary> An ascending list of the enum value names ordered alphabetically. </summary>
public static readonly IReadOnlyList<string> NamesAscending = Enum.GetNames( typeof( T ) ).OrderBy( s => s ).ToList();
/// <summary> An descending list of the enum value names ordered alphabetically. </summary>
public static readonly IReadOnlyList<string> NamesDescending = Enum.GetNames( typeof( T ) ).OrderByDescending( s => s ).ToList();
/// <summary> The smallest defined value in the enum </summary>
public static readonly T MinDefinedValue = ValuesAscending[0];
/// <summary> The largest defined value in the enum </summary>
public static readonly T MaxDefinedValue = ValuesAscending[ValuesAscending.Count-1];
/// <summary> Try to parse the given string as an enum type T </summary>
/// <remarks>
/// This method differs from the standard <see cref="System.Enum.TryParse"/>.
/// In the cases where s is a value representation, this method will check to see
/// if the value is defined in the enum or if it is a valid flag for the enum.
/// </remarks>
public static bool TryParse( string s, out T result )
{
if (!Enum.TryParse( s, out result )) { return false; }
if (HasFlagsAttribute) { return AllFlags.HasFlag( result ); }
return Enum.IsDefined( typeof( T ), result );
}
/// <summary> Try to parse the given string as an enum type T </summary>
/// <remarks>
/// This method differs from the standard <see cref="System.Enum.TryParse"/>.
/// If `onlyDefinedValues` is true, then
/// in the cases where s is a value representation, this method will check to see
/// if the value is defined in the enum or if it is a valid flag for the enum.
/// </remarks>
public static bool TryParse( string s, bool onlyDefinedValues, out T result )
{
if (!Enum.TryParse( s, out result )) { return false; }
if (!onlyDefinedValues) { return true; }
if (HasFlagsAttribute) { return AllFlags.HasFlag( result ); }
return Enum.IsDefined( typeof( T ), result );
}
/// <summary> Parse the given string as an enum type T, throwing exceptions when that fails </summary>
/// <remarks>
/// This method differs from the standard <see cref="System.Enum.TryParse"/>.
/// In the cases where s is a value representation, this method will check to see
/// if the value is defined in the enum or if it is a valid flag for the enum.
/// </remarks>
public static T Parse( string s )
{
if (!Enum.TryParse( s, out T result ))
{
throw new FormatException( $"Parameter 's' is not a valid representation of enum '{typeof(T).Name}' as string '{s}'" );
}
if (HasFlagsAttribute)
{
return AllFlags.HasFlag( result ) ? result : throw new FormatException( $"Parameter 's' is not defined by enum '{typeof(T).Name}' as string '{s}'" );
}
return Enum.IsDefined( typeof( T ), result ) ? result : throw new FormatException( $"Parameter 's' is not defined by enum '{typeof(T).Name}' as string '{s}'" );
}
/// <summary> Parse the given string as an enum type T, throwing exceptions when that fails </summary>
/// <remarks>
/// This method differs from the standard <see cref="System.Enum.TryParse"/>.
/// If `onlyDefinedValues` is true, then
/// in the cases where s is a value representation, this method will check to see
/// if the value is defined in the enum or if it is a valid flag for the enum.
/// </remarks>
public static T Parse( string s, bool onlyDefinedValues = true )
{
if (!Enum.TryParse( s, out T result ))
{
throw new FormatException( $"Parameter 's' is not a valid representation of enum '{typeof(T).Name}' as string '{s}'" );
}
if (!onlyDefinedValues) { return result; }
if (HasFlagsAttribute)
{
return AllFlags.HasFlag( result ) ? result : throw new FormatException( $"Parameter 's' is not defined by enum '{typeof(T).Name}' as string '{s}'" );
}
return Enum.IsDefined( typeof( T ), result ) ? result : throw new FormatException( $"Parameter 's' is not defined by enum '{typeof(T).Name}' as string '{s}'" );
}
/// <summary> A combination of all defined flag values in the enum </summary>
/// <remarks> Does not contain undefined flags </remarks>
public static T AllFlags => FlagsCache.AllFlags;
/// <summary> Add two sets of flags together. Same as a bitwise OR. </summary>
public static T AddFlags( T a, T b ) => FlagsCache.AddFlag( a, b );
/// <summary> Returns the flags of `a` with the flags of `b` removed. Same as a bitwise AND with inverted `b`. </summary>
public static T SubtractFlags( T a, T b ) => FlagsCache.RemoveFlag( a, b );
// Lazily instantiated, in case an enum never needs these.
internal static class FlagsCache
{
public static readonly T AllFlags;
public static readonly Func<T, T, T> AddFlag = __GetAddFlagFunc();
private static Func<T, T, T> __GetAddFlagFunc()
{
var enumType = typeof(T);
var underlyingType = Enum.GetUnderlyingType( enumType );
var p = Expression.Parameter( enumType );
var q = Expression.Parameter( enumType );
return Expression.Lambda<Func<T, T, T>>(
Expression.Convert(
Expression.Or(
Expression.Convert( p, underlyingType ),
Expression.Convert( q, underlyingType )
),
enumType
),
p, q
).Compile();
}
public static readonly Func<T, T, T> RemoveFlag = __GetRemoveFlagFunc();
private static Func<T, T, T> __GetRemoveFlagFunc()
{
var enumType = typeof(T);
var underlyingType = Enum.GetUnderlyingType( enumType );
var p = Expression.Parameter( enumType );
var q = Expression.Parameter( enumType );
return Expression.Lambda<Func<T, T, T>>(
Expression.Convert(
Expression.And(
Expression.Convert( p, underlyingType ),
Expression.Not(
Expression.Convert( q, underlyingType )
)
),
enumType
),
p, q
).Compile();
}
static FlagsCache()
{
AllFlags = ValuesAscending.Aggregate( (a,b) => AddFlag(a,b) );
}
};
/// <summary> Returns true and the next explicitly defined enum value up from the current one, if it exists. False otherwise. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static bool TryGetNext( T curr, out T result ) => ValueOrderCache.NextDict.TryGetValue( curr, out result );
/// <summary> Returns true and the next explicitly defined enum value down from the current one, if it exists. False otherwise. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static bool TryGetPrev( T curr, out T result ) => ValueOrderCache.PrevDict.TryGetValue( curr, out result );
// Lazily instantiated, in case an enum never needs these.
private static class ValueOrderCache
{
public static readonly IReadOnlyDictionary<T,T> NextDict;
public static readonly IReadOnlyDictionary<T,T> PrevDict;
static ValueOrderCache()
{
if (ValuesAscending.Count <= 0) { throw new Exception( "Enum contains no values. Cannot generate NextDict." ); }
var nextDict = new Dictionary<T, T>();
var prevDict = new Dictionary<T, T>();
// Will not contain the ending values.
for (int i = 1; i < ValuesAscending.Count -1; i++)
{
nextDict[ValuesAscending[i]] = ValuesAscending[i+1];
prevDict[ValuesAscending[i]] = ValuesAscending[i-1];
}
NextDict = nextDict;
PrevDict = prevDict;
}
}
/// <summary> Returns the next explicitly defined value up from curr. Wraps to lowest value from largest value. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static T ShiftNext( T curr ) => ValueShiftCache.NextDict[curr];
/// <summary> Returns the next explicitly defined value down from curr. Wraps to highest value from lowest value. </summary>
/// <remarks> Combined flag values that are not explicitly defined will be skipped. </remarks>
public static T ShiftPrev( T curr ) => ValueShiftCache.PrevDict[curr];
// Lazily instantiated, in case an enum never needs these.
private static class ValueShiftCache
{
public static readonly IReadOnlyDictionary<T,T> NextDict;
public static readonly IReadOnlyDictionary<T,T> PrevDict;
static ValueShiftCache()
{
if (ValuesAscending.Count <= 0) { throw new Exception( "Enum contains no values. Cannot generate NextDict." ); }
if (ValuesAscending.Count == 1)
{
// Only one value, loops back to itself
NextDict = new Dictionary<T, T>() { [ValuesAscending[0]] = ValuesAscending[0] };
PrevDict = new Dictionary<T, T>() { [ValuesAscending[0]] = ValuesAscending[0] };
return;
}
// Ending values wrap
var nextDict = new Dictionary<T, T>() { [ValuesAscending[0]] = ValuesAscending[1], [ValuesAscending[ValuesAscending.Count-1]] = ValuesAscending[0] };
var prevDict = new Dictionary<T, T>() { [ValuesAscending[0]] = ValuesAscending[ValuesAscending.Count-1], [ValuesAscending[ValuesAscending.Count-1]] = ValuesAscending[ValuesAscending.Count-2] };
// All inner values go to next or prev
for (int i = 1; i < ValuesAscending.Count -1; i++)
{
nextDict[ValuesAscending[i]] = ValuesAscending[i+1];
prevDict[ValuesAscending[i]] = ValuesAscending[i-1];
}
NextDict = nextDict;
PrevDict = prevDict;
}
}
};
}
@cgbeutler
Copy link
Author

cgbeutler commented Oct 21, 2022

I've moved this to a more official repo instead:

https://github.com/cgbeutler/CSharpEnumExt

There are also a few new features in that version, though I removed the shift value stuff, as it was a bit sketchy.

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