Friday, February 13, 2009

EnumHelper using generics to reduce casting

using System;
using System.ComponentModel;

namespace ConsoleApplication1
{
    public enum ThreadPriority
    {
        [Description("Highest")]
        Highest,
        [Description("Above Normal")]
        AboveNormal,
        [Description("Normal")]
        Normal,
        [Description("Below Normal")]
        BelowNormal,
        [Description("Lowest")]
        Lowest
    }

    class Program
    {
        static void Main(string[] args)
        {
            ThreadPriority threadPriority = EnumHelper<ThreadPriority>.Parse("AboveNormal");
            System.Diagnostics.Debug.Assert(threadPriority == ThreadPriority.AboveNormal);

            string description = EnumHelper<ThreadPriority>.EnumValueDescription(threadPriority);
            System.Diagnostics.Debug.Assert(description.Equals("Above Normal"));

            threadPriority = EnumHelper<ThreadPriority>.ParseOrDescriptionMatch("Below Normal", ThreadPriority.Normal);
            System.Diagnostics.Debug.Assert(threadPriority == ThreadPriority.BelowNormal);

            if (!EnumHelper<ThreadPriority>.TryParse("Highest", out threadPriority))
            {
                System.Diagnostics.Debug.Fail("TryParse expected to succeed");
            }
            System.Diagnostics.Debug.Assert(threadPriority == ThreadPriority.Highest);

            if (EnumHelper<ThreadPriority>.TryParse("Foo Bar", out threadPriority))
            {
                System.Diagnostics.Debug.Fail("TryParse expected to fail");
            }
        }
    }

    public static class EnumHelper<T>
       where T : struct, IComparable, IFormattable, IConvertible
    {

        /// 
        /// Static constructor to ensure T is an enum
        /// 
        static EnumHelper()
        {
            if (!typeof(T).IsEnum)
            {
                throw new ArgumentException("Type parameter must be an enum");
            }
        }     
  
        /// <summary>
        ///  Converts the string representation of the name or numeric value of one or
        ///  more enumerated constants to an equivalent enumerated object.
        /// </summary>
        /// <param name="value">A string containing the name or value to convert.</param>
        /// <returns>An object of type T whose value is represented by value.</returns>
        /// <exception cref="System.ArgumentNullException">value is null</exception>
        /// <exception cref="System.ArgumentException">value is either an empty string or
        /// only contains white space.  -or- value is a name, but not one of the named
        /// constants defined for the enumeration.
        ///</exception>
        public static T Parse(string value)
        {
            return (T)Enum.Parse(typeof(T), value);
        }

        /// 
        /// Converts the string representation of the name or numeric value of one or more enumerated constants to an equivalent enumerated object.
        /// 
        /// A string containing the name or value to convert.
        /// When this method returns, contains the object of type T whose 
        /// value is represented by the value, if the conversion succeeded, or the first 
        /// value in the enum if conversion failed. This parameter is passed uninitialized.
        /// true if value was converted successfully; otherwise, false.
        public static bool TryParse(object value, out T returnValue)
        {
            Type underlyingType = Enum.GetUnderlyingType(typeof(T));
            bool supportedType = (value is string || value.GetType().Equals(underlyingType));
            if (supportedType && Enum.IsDefined(typeof(T), value))
            {
                //direct string or underlying type match
                returnValue = Parse(value.ToString());
                return true;
            }
            else if (Enum.IsDefined(typeof(T), Convert.ChangeType(value, underlyingType))) // May throw overflow exception. E.g. long.MaxValue to Int32
            {
                //underlying numeric type match after type conversion
                returnValue = (T)Enum.Parse(typeof(T), value.ToString());
                return true;
            }
            else
            {
                //Default to the first item from the enum
                string[] values = Enum.GetNames(typeof(T));
                returnValue = (T)Enum.Parse(typeof(T), values[0], true);

                //default(T) won't work for all enums.
                //E.g. if the underlying type for the enum is int default(T) will always return 0, for which there might not be a value.
            }

            return false;
        }

        /// <summary>
        /// Attempt to read the Description Attribute
        /// </summary>
        /// <param name="e">The enum value to read the description from</param>
        /// <returns>The description value for the enum, otherwise the enum value converted to a string.</returns>
        public static string EnumValueDescription(T e)
        {
            System.Reflection.FieldInfo EnumInfo = e.GetType().GetField(e.ToString());

            System.ComponentModel.DescriptionAttribute[] enumAttributes =
                (System.ComponentModel.DescriptionAttribute[])
                EnumInfo.GetCustomAttributes(typeof(System.ComponentModel.DescriptionAttribute), false);

            if (enumAttributes.Length > 0)
            {
                return enumAttributes[0].Description;
            }
            return e.ToString();
        }

        /// <summary>
        /// Converts the string representation of the name or numeric value of one or
        ///  more enumerated constants to an equivalent enumerated object.
        ///  If a direct match isn't found a match will be attempted on the description attributes.
        /// </summary>
        /// <param name="value"></param>
        /// <param name="defaultValue"></param>
        /// <returns></returns>
        public static T ParseOrDescriptionMatch(string value, T defaultValue)
        {
            Type type = typeof(T);
            try
            {
                return Parse(value);
            }
            catch (Exception)
            {
                //Try Description Matching
                string[] names = Enum.GetNames(type);
                foreach (string name in names)
                {
                    T nameEnum = Parse(name);

                    string nameEnumValue = EnumValueDescription(nameEnum);

                    if (nameEnumValue == value)
                    {
                        return nameEnum;
                    }
                }

                return defaultValue;
            }
        }
    }
}