How do I create a generic converter for units of measurement in C#?

Pricey picture Pricey · Oct 21, 2011 · Viewed 11k times · Source

I have been trying to learn a bit more about delegates and lambdas while working on a small cooking project that involves temperature conversion as well as some cooking measurement conversions such as Imperial to Metric and I've been trying to think of a way to make an extensible Unit converter.

Here is what I started with, along with code comments on what some of my plans were. I have no plan to use it like the below, I was just testing out some features of C# I don't know very well, I am also unsure how to take this further. Does anyone have any suggestions on how to create what I am talking about in the comments below? Thanks

namespace TemperatureConverter
{
    class Program
    {
        static void Main(string[] args)
        {
            // Fahrenheit to Celsius :  [°C] = ([°F] − 32) × 5⁄9
            var CelsiusResult = Converter.Convert(11M,Converter.FahrenheitToCelsius);

            // Celsius to Fahrenheit : [°F] = [°C] × 9⁄5 + 32
            var FahrenheitResult = Converter.Convert(11M, Converter.CelsiusToFahrenheit);

            Console.WriteLine("Fahrenheit to Celsius : " + CelsiusResult);
            Console.WriteLine("Celsius to Fahrenheit : " + FahrenheitResult);
            Console.ReadLine();

            // If I wanted to add another unit of temperature i.e. Kelvin 
            // then I would need calculations for Kelvin to Celsius, Celsius to Kelvin, Kelvin to Fahrenheit, Fahrenheit to Kelvin
            // Celsius to Kelvin : [K] = [°C] + 273.15
            // Kelvin to Celsius : [°C] = [K] − 273.15
            // Fahrenheit to Kelvin : [K] = ([°F] + 459.67) × 5⁄9
            // Kelvin to Fahrenheit : [°F] = [K] × 9⁄5 − 459.67
            // The plan is to have the converters with a single purpose to convert to
            //one particular unit type e.g. Celsius and create separate unit converters 
            //that contain a list of calculations that take one specified unit type and then convert to their particular unit type, in this example its Celsius.
        }
    }

    // at the moment this is a static class but I am looking to turn this into an interface or abstract class
    // so that whatever implements this interface would be supplied with a list of generic deligate conversions
    // that it can invoke and you can extend by adding more when required.
    public static class Converter
    {
        public static Func<decimal, decimal> CelsiusToFahrenheit = x => (x * (9M / 5M)) + 32M;
        public static Func<decimal, decimal> FahrenheitToCelsius = x => (x - 32M) * (5M / 9M);

        public static decimal Convert(decimal valueToConvert, Func<decimal, decimal> conversion) {
            return conversion.Invoke(valueToConvert);
        }
    }
}

Update: Trying to clarify my question:

Using just my temperature example below, how would I create a class that contains a list of lambda conversions to Celsius which you then pass it a given temperature and it will try and convert that to Celsius (if the calculation is available)

Example pseudo code:

enum Temperature
{
    Celcius,
    Fahrenheit,
    Kelvin
}

UnitConverter CelsiusConverter = new UnitConverter(Temperature.Celsius);
CelsiusConverter.AddCalc("FahrenheitToCelsius", lambda here);
CelsiusConverter.Convert(Temperature.Fahrenheit, 11);

Answer

Danny Tuppeny picture Danny Tuppeny · Oct 21, 2011

I thought this was an interesting little problem, so I decided to see how nicely this could be wrapped up into a generic implementation. This isn't well-tested (and doesn't handle all error cases - such as if you don't register the conversion for a particular unit type, then pass that in), but it might be useful. The focus was on making the inherited class (TemperatureConverter) as tidy as possible.

/// <summary>
/// Generic conversion class for converting between values of different units.
/// </summary>
/// <typeparam name="TUnitType">The type representing the unit type (eg. enum)</typeparam>
/// <typeparam name="TValueType">The type of value for this unit (float, decimal, int, etc.)</typeparam>
abstract class UnitConverter<TUnitType, TValueType>
{
    /// <summary>
    /// The base unit, which all calculations will be expressed in terms of.
    /// </summary>
    protected static TUnitType BaseUnit;

    /// <summary>
    /// Dictionary of functions to convert from the base unit type into a specific type.
    /// </summary>
    static ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>> ConversionsTo = new ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>>();

    /// <summary>
    /// Dictionary of functions to convert from the specified type into the base unit type.
    /// </summary>
    static ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>> ConversionsFrom = new ConcurrentDictionary<TUnitType, Func<TValueType, TValueType>>();

    /// <summary>
    /// Converts a value from one unit type to another.
    /// </summary>
    /// <param name="value">The value to convert.</param>
    /// <param name="from">The unit type the provided value is in.</param>
    /// <param name="to">The unit type to convert the value to.</param>
    /// <returns>The converted value.</returns>
    public TValueType Convert(TValueType value, TUnitType from, TUnitType to)
    {
        // If both From/To are the same, don't do any work.
        if (from.Equals(to))
            return value;

        // Convert into the base unit, if required.
        var valueInBaseUnit = from.Equals(BaseUnit)
                                ? value
                                : ConversionsFrom[from](value);

        // Convert from the base unit into the requested unit, if required
        var valueInRequiredUnit = to.Equals(BaseUnit)
                                ? valueInBaseUnit
                                : ConversionsTo[to](valueInBaseUnit);

        return valueInRequiredUnit;
    }

    /// <summary>
    /// Registers functions for converting to/from a unit.
    /// </summary>
    /// <param name="convertToUnit">The type of unit to convert to/from, from the base unit.</param>
    /// <param name="conversionTo">A function to convert from the base unit.</param>
    /// <param name="conversionFrom">A function to convert to the base unit.</param>
    protected static void RegisterConversion(TUnitType convertToUnit, Func<TValueType, TValueType> conversionTo, Func<TValueType, TValueType> conversionFrom)
    {
        if (!ConversionsTo.TryAdd(convertToUnit, conversionTo))
            throw new ArgumentException("Already exists", "convertToUnit");
        if (!ConversionsFrom.TryAdd(convertToUnit, conversionFrom))
            throw new ArgumentException("Already exists", "convertToUnit");
    }
}

The generic type args are for an enum that represents the units, and the type for the value. To use it, you just have to inherit from this class (providing the types) and register some lambdas to do the conversion. Here's an example for temperature (with some dummy calculations):

enum Temperature
{
    Celcius,
    Fahrenheit,
    Kelvin
}

class TemperatureConverter : UnitConverter<Temperature, float>
{
    static TemperatureConverter()
    {
        BaseUnit = Temperature.Celcius;
        RegisterConversion(Temperature.Fahrenheit, v => v * 2f, v => v * 0.5f);
        RegisterConversion(Temperature.Kelvin, v => v * 10f, v => v * 0.05f);
    }
}

And then using it is pretty simple:

var converter = new TemperatureConverter();

Console.WriteLine(converter.Convert(1, Temperature.Celcius, Temperature.Fahrenheit));
Console.WriteLine(converter.Convert(1, Temperature.Fahrenheit, Temperature.Celcius));

Console.WriteLine(converter.Convert(1, Temperature.Celcius, Temperature.Kelvin));
Console.WriteLine(converter.Convert(1, Temperature.Kelvin, Temperature.Celcius));

Console.WriteLine(converter.Convert(1, Temperature.Kelvin, Temperature.Fahrenheit));
Console.WriteLine(converter.Convert(1, Temperature.Fahrenheit, Temperature.Kelvin));