Click here to Skip to main content
15,881,380 members
Articles / Programming Languages / C#

Working with Units and Amounts

Rate me:
Please Sign up or sign in to vote.
4.99/5 (76 votes)
17 Jul 2013CPOL17 min read 87.1K   1.5K   150  
The ultimate Unit and Amount classes for your business and physics applications!
using System;
using System.Collections.Generic;
using System.Reflection;

namespace TypedUnits
{
    /// <summary>
    /// An event handler called whenever unit names cannot be resolved. Provides a last chance
    /// to resolve units.
    /// </summary>
    public delegate Unit UnitResolveEventHandler(object sender, ResolveEventArgs args);

    /// <summary>
    /// Delegate representiong a unitdirectional unit conversion function.
    /// </summary>
    /// <param name="originalAmount">The amount to be converted.</param>
    /// <returns>The resulting amount.</returns>
    public delegate Amount ConversionFunction(Amount originalAmount);

    /// <summary>
    /// The UnitManager class provides services around unit naming and identification.
    /// </summary>
    /// <remarks>
    /// The UnitManager class contains static methods that access a singleton instance 
    /// of the class.
    /// </remarks>
    public sealed class UnitManager
    {
        #region Fields

        private static UnitManager instance;

        // Stores for named units:
        private List<Unit> allUnits = new List<Unit>();
        private Dictionary<UnitType, List<Unit>> unitsByType = new Dictionary<UnitType, List<Unit>>();
        private Dictionary<string, Unit> unitsByName = new Dictionary<string, Unit>();
        private Dictionary<string, Unit> unitsBySymbol = new Dictionary<string, Unit>();

        // Store for conversion functions:
        private Dictionary<UnitConversionKeySlot, UnitConversionValueSlot> conversions = new Dictionary<UnitConversionKeySlot, UnitConversionValueSlot>();

        #endregion Fields

        #region Public properties

        /// <summary>
        /// The instance of the currently used UnitManager.
        /// </summary>
        public static UnitManager Instance
        {
            get
            {
                if (UnitManager.instance == null)
                {
                    UnitManager.instance = new UnitManager();
                }
                return UnitManager.instance;
            }
            set { UnitManager.instance = value; }
        }

        #endregion Public properties

        #region Public methods - Registrations

        /// <summary>
        /// Registers both units and conversions based on the assemblies public types marked
        /// with [UnitDefinitionsClass] and [UnitConversionsClass] attributes.
        /// </summary>
        public static void RegisterByAssembly(Assembly assembly)
        {
            RegisterUnits(assembly);
            RegisterConversions(assembly);
        }

        /// <summary>
        /// Register a conversion function.
        /// </summary>
        /// <param name="fromUnit">The unit from which this conversion function allows conversion.</param>
        /// <param name="toUnit">The unit to which this conversion function allows conversion to.</param>
        /// <param name="conversionFunction">The unit conversion function.</param>
        /// <remarks>
        /// A unit conversion function is registered to convert from one unit to another. It will
        /// however be applied to convert from any unit of the same family of the fromUnit, to any
        /// unit family of the toUnit. For reverse conversion, a separate function must be registered.
        /// </remarks>
        public static void RegisterConversion(Unit fromUnit, Unit toUnit, ConversionFunction conversionFunction)
        {
            Instance.conversions[new UnitConversionKeySlot(fromUnit, toUnit)] = new UnitConversionValueSlot(fromUnit, toUnit, conversionFunction);
        }

        /// <summary>
        /// Registers a set of conversion functions by executing all public static void methods of 
        /// the given type. The methods are supposed to call the RegisterConversion method to register
        /// individual conversion functions.
        /// </summary>
        public static void RegisterConversions(Type unitConversionsClass)
        {
            object[] none = new object[0];
            foreach (MethodInfo method in unitConversionsClass.GetMethods(BindingFlags.InvokeMethod | BindingFlags.Public | BindingFlags.Static))
            {
                if ((method.ReturnType == typeof(void)) && (method.GetParameters().Length == 0))
                {
                    method.Invoke(null, none);
                }
            }
        }

        /// <summary>
        /// Registers a set of conversion function by executing all public static void methods
        /// of public types marked with the [UnitConversionsClass] attribute in the given assembly.
        /// The methods are supposed to call the RegisterConversion method to register individual
        /// conversion functions.
        /// </summary>
        public static void RegisterConversions(Assembly assembly)
        {
            foreach (Type t in assembly.GetExportedTypes())
            {
                if (t.GetCustomAttributes(typeof(UnitConversionClassAttribute), false).Length > 0)
                {
                    RegisterConversions(t);
                }
            }
        }

        /// <summary>
        /// Event raised whenever a unit can not be resolved.
        /// </summary>
        public event UnitResolveEventHandler UnitResolve;

        /// <summary>
        /// Registers a unit.
        /// </summary>
        public static void RegisterUnit(Unit unit)
        {
            // Check precondition: unit <> null
            if (unit == null) throw new ArgumentNullException("unit");

            // Check if unit already registered:
            foreach (Unit u in Instance.allUnits)
            {
                if (Object.ReferenceEquals(u, unit)) return;
            }

            // Register unit in allUnits:
            Instance.allUnits.Add(unit);

            // Register unit in unitsByType:
            try
            {
                Instance.unitsByType[unit.UnitType].Add(unit);
            }
            catch (KeyNotFoundException)
            {
                Instance.unitsByType[unit.UnitType] = new List<Unit>();
                Instance.unitsByType[unit.UnitType].Add(unit);
            }

            // Register unit by name and symbol:
            Instance.unitsByName[unit.Name] = unit;
            Instance.unitsBySymbol[unit.Symbol] = unit;
        }

        /// <summary>
        /// Register all public static fields of type Unit of the given class.
        /// </summary>
        public static void RegisterUnits(Type unitDefinitionClass)
        {
            foreach (FieldInfo field in unitDefinitionClass.GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static))
            {
                if (field.FieldType == typeof(Unit))
                {
                    RegisterUnit((Unit)field.GetValue(null));
                }
            }
        }

        /// <summary>
        /// Registers all public static fields of type Unit of classes
        /// marked with the [UnitDefinitionClass] attribute in the given
        /// assembly.
        /// </summary>
        public static void RegisterUnits(Assembly assembly)
        {
            foreach (Type t in assembly.GetExportedTypes())
            {
                if (t.GetCustomAttributes(typeof(UnitDefinitionClassAttribute), false).Length > 0)
                {
                    RegisterUnits(t);
                }
            }
        }

        #endregion Public methods - Registrations

        #region Public methods - Named units

        /// <summary>
        /// Retrieves the unit based on its name.
        /// If the unit is not found, a UnitResolve event is fired as last chance
        /// to resolve the unit.
        /// If the unit cannot be resolved, an UnknownUnitException is raised.
        /// </summary>
        public static Unit GetUnitByName(string name)
        {
            Unit result = null;

            // Try resolve unit by unitsByName:
            Instance.unitsByName.TryGetValue(name, out result);

            // Try resolve unit by UnitResolve event:
            if (result == null)
            {
                if (Instance.UnitResolve != null)
                {
                    foreach (UnitResolveEventHandler handler in Instance.UnitResolve.GetInvocationList())
                    {
                        result = handler(Instance, new ResolveEventArgs(name));
                        if (result != null)
                        {
                            RegisterUnit(result);
                            break;
                        }
                    }
                }
            }

            // Throw exception if unit resolution failed:
            if (result == null)
            {
                throw new UnknownUnitException(String.Format("No unit found named '{0}'.", name));
            }

            // Return result:
            return result;
        }

        /// <summary>
        /// Retrieves the unit based on its symbol.
        /// If the unit is not found, an UnknownUnitException is raised.
        /// </summary>
        public static Unit GetUnitBySymbol(string symbol)
        {
            Unit result = null;

            // Try resolve unit by unitsBySymbol:
            Instance.unitsBySymbol.TryGetValue(symbol, out result);

            // Throw exception if unit resolution failed:
            if (result == null)
            {
                throw new UnknownUnitException(String.Format("No unit found with symbol '{0}'.", symbol));
            }

            // Return result:
            return result;
        }

        /// <summary>
        /// Returns the unit types for which one or more units are registered.
        /// </summary>
        public static ICollection<UnitType> GetUnitTypes()
        {
            return Instance.unitsByType.Keys;
        }

        /// <summary>
        /// Returns all registered units.
        /// </summary>
        public static IList<Unit> GetUnits()
        {
            return Instance.allUnits;
        }

        /// <summary>
        /// Whether the given unit is already registered to the UnitManager.
        /// </summary>
        public static bool IsRegistered(Unit unit)
        {
            return (Instance.allUnits.Contains(unit));
        }

        /// <summary>
        /// Returns all registered units of the given type.
        /// </summary>
        public static IList<Unit> GetUnits(UnitType unitType)
        {
            return Instance.unitsByType[unitType];
        }

        /// <summary>
        /// Returns a registered unit that matches the given unit.
        /// </summary>
        /// <param name="unit">The unit for which to find a registered match.</param>
        /// <param name="selfIfNone">
        /// If true, returns the passed unit if no match is found,
        /// otherwise return null if no match is found.
        /// </param>
        /// <remarks>
        /// If the passed unit is named, the passed unit will be returned without
        /// checking if it is registered.
        /// </remarks>
        public static Unit ResolveToNamedUnit(Unit unit, bool selfIfNone)
        {
            if (unit.IsNamed) return unit;
            double factor = unit.Factor;
            if (Instance.unitsByType.ContainsKey(unit.UnitType))
            {
                foreach (Unit m in Instance.unitsByType[unit.UnitType])
                {
                    if (m.Factor == factor) return m;
                }
            }
            return (selfIfNone) ? unit : null;
        }

        #endregion Public methods - Named units

        #region Public methods - Unit conversions

        /// <summary>
        /// Converts the given amount to the given unit.
        /// </summary>
        public static Amount ConvertTo(Amount amount, Unit toUnit)
        {
            try
            {
                // Performance optimalization:
                if (Object.ReferenceEquals(amount.Unit, toUnit))
                {
                    return amount;
                }

                // Perform conversion:
                if (amount.Unit.IsCompatibleTo(toUnit))
                {
                    return new Amount(amount.Value * amount.Unit.Factor / toUnit.Factor, toUnit);
                }
                else
                {
                    UnitConversionKeySlot expectedSlot = new UnitConversionKeySlot(amount.Unit, toUnit);
                    return Instance.conversions[expectedSlot].Convert(amount).ConvertedTo(toUnit);
                }
            }
            catch (KeyNotFoundException)
            {
                throw new UnitConversionException(amount.Unit, toUnit);
            }
        }

        #endregion Public methods - Unit conversions

        #region Private classes to represent slots in conversion dictionary

        /// <summary>
        /// Key slot in the internal conversions dictionary.
        /// </summary>
        private class UnitConversionKeySlot
        {
            private UnitType fromType, toType;

            public UnitConversionKeySlot(Unit from, Unit to)
            {
                this.fromType = from.UnitType;
                this.toType = to.UnitType;
            }

            public override bool Equals(object obj)
            {
                UnitConversionKeySlot other = obj as UnitConversionKeySlot;
                return (this.fromType == other.fromType) && (this.toType == other.toType);
            }

            public override int GetHashCode()
            {
                return this.fromType.GetHashCode() ^ this.toType.GetHashCode();
            }
        }

        /// <summary>
        /// Value slot in the internal conversions dictionary.
        /// </summary>
        private class UnitConversionValueSlot
        {
            private Unit from, to;
            private ConversionFunction conversionFunction;

            public UnitConversionValueSlot(Unit from, Unit to, ConversionFunction conversionFunction)
            {
                this.from = from;
                this.to = to;
                this.conversionFunction = conversionFunction;
            }

            public Amount Convert(Amount amount)
            {
                return this.conversionFunction(amount.ConvertedTo(from));
            }
        }

        #endregion Private classes to represent slots in conversion dictionary	
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AREBIS
Belgium Belgium
Senior Software Architect and independent consultant.

Comments and Discussions