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);
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.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):
And then using it is pretty simple:
You could take a look at Units.NET. It's on GitHub and NuGet. It provides most common units and conversions, supports both static typing and enumeration of units and parsing/printing of abbreviations. It doesn't parse expressions though, and you can't extend existing classes of units, but you can extend it with new third party units.
Example conversions:
One can define a physical units generic type such that, if one has for each unit a type which implements
new
and includes a translation method between that unit and a "base unit" of that type, one can perform arithmetic on values that are expressed in different units and have them converted as necessary, using the type system such that a variable of typeAreaUnit<LengthUnit.Inches>
would only accept things dimensioned in square inches, but if one saidmyAreaInSquareInches= AreaUnit<LengthUnit.Inches>.Product(someLengthInCentimeters, someLengthInFathoms);
it would automatically translate those other unit before performing the multiplication. It can actually work out pretty well when using method-call syntax since methods likeProduct<T1,T2>(T1 p1, T2 p2)
method can accept generic type parameters their operands. Unfortunately, there's no way to make operators generic, nor is there way for a type likeAreaUnit<T> where T:LengthUnitDescriptor
to define a means of conversion to or from some other arbitrary generic typeAreaUnit<U>
. AnAreaUnit<T>
could define conversions to and from e.g.AreaUnit<Angstrom>
, but there's no way the compiler could be told that code which is given anAreaUnit<Centimeters> and wants
AreaUnit` can convert inches to angstroms and then to centimeters.It sounds like you want something like:
However, you might want to consider not just using
decimal
, but having a type which knows the units so you can't accidentally (say) apply the "Celsius to Kelvin" conversion to a non-Celsius value. Possibly have a look at the F# Units of Measure approach for inspiration.You have a good start, but like Jon said, it's currently not type-safe; the converter has no error-checking to ensure the decimal it gets is a Celsius value.
So, to take this further, I would start introducing struct types that take the numerical value and apply it to a unit of measure. In the Patterns of Enterprise Architecture (aka the Gang of Four design patterns), this is called the "Money" pattern after the most common usage, to denote an amount of a type of currency. The pattern holds for any numeric amount that requires a unit of measure to be meaningful.
Example:
Now, you have a simple type that you can use to enforce that the conversions you are making take a Temperature that is of the proper scale, and return a result Temperature known to be in the other scale.
Your Funcs will need an extra line to check that the input matches the output; you can continue to use lambdas, or you can take it one step further with a simple Strategy pattern:
Because these types of conversions are two-way, you may consider setting up the interface to handle both ways, with a "ConvertBack" method or similar that will take a Temperature in the Celsius scale and convert to Fahrenheit. That reduces your class count. Then, instead of class instances, your dictionary values could be pointers to methods on instances of the converters. This increases the complexity somewhat of setting up the main TemperatureConverter strategy-picker, but reduces the number of conversion strategy classes you must define.
Also notice that the error-checking is done at runtime when you are actually trying to make the conversion, requiring this code to be tested thoroughly in all usages to ensure it's always correct. To avoid this, you can derive the base Temperature class to produce CelsiusTemperature and FahrenheitTemperature structs, which would simply define their Scale as a constant value. Then, the ITemperatureConverter could be made generic to two types, both Temperatures, giving you compile-time checking that you are specifying the conversion you think you are. the TemperatureConverter can also be made to dynamically find ITemperatureConverters, determine the types they will convert between, and automagically set up the dictionary of converters so you never have to worry about adding new ones. This comes at the cost of increased Temperature-based class count; you'll need four domain classes (a base and three derived classes) instead of one. It will also slow the creation of a TemperatureConverter class as the code to reflectively build the converter dictionary will use quite a bit of reflection.
You could also change the enums for the units of measure to become "marker classes"; empty classes that have no meaning other than that they are of that class and derive from other classes. You could then define a full hierarchy of "UnitOfMeasure" classes that represent the various units of measure, and can be used as generic type arguments and constraints; ITemperatureConverter could be generic to two types, both of which are constrained to be TemperatureScale classes, and a CelsiusFahrenheitConverter implementation would close the generic interface to the types CelsiusDegrees and FahrenheitDegrees both derived from TemperatureScale. That allows you to expose the units of measure themselves as constraints of a conversion, in turn allowing conversions between types of units of measure (certain units of certain materials have known conversions; 1 British Imperial Pint of water weighs 1.25 pounds).
All of these are design decisions that will simplify one type of change to this design, but at some cost (either making something else harder to do or decreasing algorithm performance). It's up to you to decide what's really "easy" for you, in the context of the overall application and coding environment you work in.
EDIT: The usage you want, from your edit, is extremely easy for temperature. However, if you want a generic UnitConverter that can work with any UnitofMeasure, then you no longer want Enums to represent your units of measure, because Enums can't have a custom inheritance hierarchy (they derive directly from System.Enum).
You can specify that the default constructor can accept any Enum, but then you have to ensure that the Enum is one of the types that is a unit of measure, otherwise you could pass in a DialogResult value and the converter would freak out at runtime.
Instead, if you want one UnitConverter that can convert to any UnitOfMeasure given lambdas for other units of measure, I would specify the units of measure as "marker classes"; small stateless "tokens" that only have meaning in that they are their own type and derive from their parents:
You can put in error-checking (check that the input unit has a conversion specified, check that a conversion being added is for a UOM with the same parent as the base type, etc etc) as you see fit. You can also derive UnitConverter to create TemperatureConverter, allowing you to add static, compile-time type checks and avoiding the run-time checks that UnitConverter would have to use.
Normally I wanted to add this as a comment to Danny Tuppeny's post, but it seems that I am not able to add this as comment.
I improved the solution from @Danny Tuppeny a little bit. I did not want to add each conversion with two conversation factors, because only one should be necessary. Also the parameter of type Func does not seems to be necessary, it only makes it more complicated for the user.
So my call would looks like:
I also added a method to get the conversion factor between to units. This is the modified UnitConverter class: