Last active
October 21, 2022 02:05
-
-
Save cgbeutler/699e96f4868a7ccdd004d1daf38407d9 to your computer and use it in GitHub Desktop.
A set of useful enumeration extensions
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.