How to create a common unit converter in C #?

I tried to learn a little more about delegates and lambdas while working on a small cooking project that includes temperature conversions, as well as some conversions for measuring the number of purchases, such as Imperial to Metric, and I was trying to come up with a way to create an expansion converter.

Here's what I started with code comments about what some of my plans were. I have no plan to use it, as shown below, I just tested some C # functions. I do not know very well, I am also not sure how to do this further. Does anyone have any suggestions on how to create what I'm 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 ie 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 eg 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 only my temperature example below, how can I create a class that contains a list of lambda conversions for Celsius, which you will then give it the set temperature, and try to convert it to Celsius (if the calculation is available)

Pseudo-code example:

 enum Temperature { Celcius, Fahrenheit, Kelvin } UnitConverter CelsiusConverter = new UnitConverter(Temperature.Celsius); CelsiusConverter.AddCalc("FahrenheitToCelsius", lambda here); CelsiusConverter.Convert(Temperature.Fahrenheit, 11); 
+6
source share
6 answers

I thought this was an interesting little problem, so I decided to see how beautiful it could be included in the overall implementation. This is not verified (and does not handle all cases of errors - for example, if you do not register the conversion for a specific type of device and then pass it on), but it can be useful. The focus was on the inherited class ( TemperatureConverter ).

 /// <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"); } } 

Generic type arguments are an enumeration that represents units, and a type for a value. To use it, you just need to inherit this class (by providing types) and register some lambda for conversion. Here's an example of temperature (with some fictitious 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 its use is quite 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)); 
+21
source

You have a good start, but, as John said, he is currently not type safe; the converter does not have error checking to ensure that the decimal value it receives is Celsius.

So, to take this further, I would begin to introduce types of structures that take a numerical value and apply it to a unit of measure. In enterprise architecture templates (the so-called gang of four design templates), this is called the "Money" template after its most common use to indicate the amount of a currency type. The template is executed for any numerical sum that requires a unit of measure in order to make sense.

Example:

 public enum TemperatureScale { Celsius, Fahrenheit, Kelvin } public struct Temperature { decimal Degrees {get; private set;} TemperatureScale Scale {get; private set;} public Temperature(decimal degrees, TemperatureScale scale) { Degrees = degrees; Scale = scale; } public Temperature(Temperature toCopy) { Degrees = toCopy.Degrees; Scale = toCopy.Scale; } } 

Now you have a simple type that you can use to ensure that your transformations take a temperature that has the correct scale and return a result that is known to be on a different scale.

Your Funcs will need an extra line to verify that the input matches the output; you can continue to use lambdas, or you can do it one more step with a simple strategy template:

 public interface ITemperatureConverter { public Temperature Convert(Temperature input); } public class FahrenheitToCelsius:ITemperatureConverter { public Temperature Convert(Temperature input) { if (input.Scale != TemperatureScale.Fahrenheit) throw new ArgumentException("Input scale is not Fahrenheit"); return new Temperature(input.Degrees * 5m / 9m - 32, TemperatureScale.Celsius); } } //Implement other conversion methods as ITemperatureConverters public class TemperatureConverter { public Dictionary<Tuple<TemperatureScale, TemperatureScale>, ITemperatureConverter> converters = new Dictionary<Tuple<TemperatureScale, TemperatureScale>, ITemperatureConverter> { {Tuple.Create<TemperatureScale.Fahrenheit, TemperatureScale.Celcius>, new FahrenheitToCelsius()}, {Tuple.Create<TemperatureScale.Celsius, TemperatureScale.Fahrenheit>, new CelsiusToFahrenheit()}, ... } public Temperature Convert(Temperature input, TemperatureScale toScale) { if(!converters.ContainsKey(Tuple.Create(input.Scale, toScale)) throw new InvalidOperationException("No converter available for this conversion"); return converters[Tuple.Create(input.Scale, toScale)].Convert(input); } } 

Since these types of conversions are two-way, you might consider setting up the interface for processing in both directions using the "ConvertBack" method or a similar method that will take the temperature in Celsius and convert to degrees Fahrenheit. This reduces your class. Then, instead of class instances, your dictionary values ​​can be pointers to methods on converter instances. This complicates the setup of the main StrategyConverter choice somewhat, but reduces the number of conversion strategy classes that you must define.

Also note that error checking is performed at run time when you are actually trying to do the conversion, requiring that this code be thoroughly tested in all cases of use to ensure that it is correct. To avoid this, you can get a base temperature class to create CelsiusTemperature and FahrenheitTemperature structures that simply define their scale as a constant value. Then the ITemperatureConverter could be made common to two types: temperature, giving you a compile-time check when you specify the transformation that you think. A temperature converter can also be made to dynamically search for ITemperatureConverters, determine the types that they will convert between them, and automatically configure the converter dictionary, so you don’t have to worry about adding new ones. This is due to an increase in the number of classes based on temperature; you will need four domain classes (base and three derived classes) instead of one. It will also slow down the creation of the TemperatureConverter class, as the code for smoothly creating the converter dictionary will use quite a bit of reflection.

You can also change the enumerations so that the units become "marker classes"; empty classes that have no meaning except that they belong to this class and come from other classes. Then you can define a complete hierarchy of UnitOfMeasure classes that represent different units of measure, and can be used as arguments and constraints of a general type; An ITemperatureConverter can be common to two types, both of which are limited to TemperatureScale classes, and the CelsiusFahrenheitConverter implementation closes the common interface for the CelsiusDegrees and FahrenheitDegrees types derived from TemperatureScale. This allows you to set units of measure on their own as conversion restrictions, which in turn allows conversions between unit types (certain units of certain materials have known conversions, 1 British imperial water pin weighs 1.25 pounds).

All these are design decisions that will simplify one type of changes in this design, but at a certain price (either to do something even more difficult than to do or reduce the performance of the algorithm). It's up to you to decide what is really “easy” for you, in the context of the general application and coding environment in which you work.

EDIT: The use you want from your edit is extremely simple for temperature. However, if you want the universal UnitConverter to work with any UnitofMeasure, you no longer want Enums to display your units of measure because Enums cannot have their own inheritance hierarchy (they are derived directly from System.Enum).

You can specify that the default constructor can accept any Enum, but then you need to make sure that Enum is one of the types that is the unit of measure, otherwise you could pass the DialogResult value and the converter will freak out at runtime.

Instead, if you want one UnitConverter that can convert to any UnitOfMeasure given by lambdas for other units, I would specify units as "marker classes"; small stateless "tokens" that make sense only in that they are their own type and come from their parents:

 //The only functionality any UnitOfMeasure needs is to be semantically equatable //with any other reference to the same type. public abstract class UnitOfMeasure:IEquatable<UnitOfMeasure> { public override bool Equals(UnitOfMeasure other) { return this.ReferenceEquals(other) || this.GetType().Name == other.GetType().Name; } public override bool Equals(Object other) { return other is UnitOfMeasure && this.Equals(other as UnitOfMeasure); } public override operator ==(Object other) {return this.Equals(other);} public override operator !=(Object other) {return this.Equals(other) == false;} } public abstract class Temperature:UnitOfMeasure { public static CelsiusTemperature Celsius {get{return new CelsiusTemperature();}} public static FahrenheitTemperature Fahrenheit {get{return new CelsiusTemperature();}} public static KelvinTemperature Kelvin {get{return new CelsiusTemperature();}} } public class CelsiusTemperature:Temperature{} public class FahrenheitTemperature :Temperature{} public class KelvinTemperature :Temperature{} ... public class UnitConverter { public UnitOfMeasure BaseUnit {get; private set;} public UnitConverter(UnitOfMeasure baseUnit) {BaseUnit = baseUnit;} private readonly Dictionary<UnitOfMeasure, Func<decimal, decimal>> converters = new Dictionary<UnitOfMeasure, Func<decimal, decimal>>(); public void AddConverter(UnitOfMeasure measure, Func<decimal, decimal> conversion) { converters.Add(measure, conversion); } public void Convert(UnitOfMeasure measure, decimal input) { return converters[measure](input); } } 

You can put an error check (check that the input block has the specified conversion, make sure that the conversion to be added is intended for the UOM with the same parent as the base type, etc.) as you see fit. You can also get a UnitConverter to create a TemperatureConverter, allowing you to add static compile-time checks and avoid the run-time checks that UnitConverter will have to use.

+5
source

It looks like you want something like:

 Func<decimal, decimal> celsiusToKelvin = x => x + 273.15m; Func<decimal, decimal> kelvinToCelsius = x => x - 273.15m; Func<decimal, decimal> fahrenheitToKelvin = x => ((x + 459.67m) * 5m) / 9m; Func<decimal, decimal> kelvinToFahrenheit = x => ((x * 9m) / 5m) - 459.67m; 

However, you may need to not only use decimal , but have a type that knows units, so you cannot accidentally (say) apply the transformation "Celsius to Kelvin" to a value other than virgin. Perhaps look at F # Units of Measure for inspiration.

+3
source

You can take a look at Units.NET. This is on GitHub and NuGet . It provides the most common units and conversions, supports both static typing and unit listing, as well as parsing / printing of abbreviations. However, it does not parse expressions, and you cannot extend existing unit classes, but you can extend it with new third-party blocks.

Conversion Examples:

 Length meter = Length.FromMeters(1); double cm = meter.Centimeters; // 100 double feet = meter.Feet; // 3.28084 
+1
source

Usually I would like to add this as a comment on a Danny Tuppeny post, but it seems like I cannot add this as a comment.

I slightly improved the solution from @Danny Tuppeny. I did not want to add each transformation with two conversation factors, because only one was needed. Also, a parameter of type Func is not needed, it only complicates the user.

So my call would look like this:

 public enum TimeUnit { Milliseconds, Second, Minute, Hour, Day, Week } public class TimeConverter : UnitConverter<TimeUnit, double> { static TimeConverter() { BaseUnit = TimeUnit.Second; RegisterConversion(TimeUnit.Milliseconds, 1000); RegisterConversion(TimeUnit.Minute, 1/60); RegisterConversion(TimeUnit.Hour, 1/3600); RegisterConversion(TimeUnit.Day, 1/86400); RegisterConversion(TimeUnit.Week, 1/604800); } } 

I also added a method to get the conversion factor between units. This is a modified UnitConverter class:

 /// <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> /// <remarks>http://stackoverflow.com/questions/7851448/how-do-i-create-a-generic-converter-for-units-of-measurement-in-c /// </remarks> public abstract class UnitConverter<TUnitType, TValueType> where TValueType : struct, IComparable, IComparable<TValueType>, IEquatable<TValueType>, IConvertible { /// <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; } public double ConversionFactor(TUnitType from, TUnitType to) { return Convert(One(), from, to).ToDouble(CultureInfo.InvariantCulture); } /// <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="conversionToFactor">a factor converting into the base unit.</param> protected static void RegisterConversion(TUnitType convertToUnit, TValueType conversionToFactor) { if (!ConversionsTo.TryAdd(convertToUnit, v=> Multiply(v, conversionToFactor))) throw new ArgumentException("Already exists", "convertToUnit"); if (!ConversionsFrom.TryAdd(convertToUnit, v => MultiplicativeInverse(conversionToFactor))) throw new ArgumentException("Already exists", "convertToUnit"); } static TValueType Multiply(TValueType a, TValueType b) { // declare the parameters ParameterExpression paramA = Expression.Parameter(typeof(TValueType), "a"); ParameterExpression paramB = Expression.Parameter(typeof(TValueType), "b"); // add the parameters together BinaryExpression body = Expression.Multiply(paramA, paramB); // compile it Func<TValueType, TValueType, TValueType> multiply = Expression.Lambda<Func<TValueType, TValueType, TValueType>>(body, paramA, paramB).Compile(); // call it return multiply(a, b); } static TValueType MultiplicativeInverse(TValueType b) { // declare the parameters ParameterExpression paramA = Expression.Parameter(typeof(TValueType), "a"); ParameterExpression paramB = Expression.Parameter(typeof(TValueType), "b"); // add the parameters together BinaryExpression body = Expression.Divide(paramA, paramB); // compile it Func<TValueType, TValueType, TValueType> divide = Expression.Lambda<Func<TValueType, TValueType, TValueType>>(body, paramA, paramB).Compile(); // call it return divide(One(), b); } //Returns the value "1" as converted Type static TValueType One() { return (TValueType) System.Convert.ChangeType(1, typeof (TValueType)); } } 
0
source

You can determine the general type of physical device in such a way that if for each unit there is a type that implements new , and includes a translation method between this unit and the "base unit" of this type, you can perform arithmetic by the values ​​expressed in different units, and their conversion if necessary using a type system, so that a variable of type AreaUnit<LengthUnit.Inches> will only accept values ​​that are square inches, but if one of them is specified myAreaInSquareInches= AreaUnit<LengthUnit.Inches>.Product(someLengthInCentimeters, someLengthInFathoms); , it automatically converts these other units before doing the multiplication. This can work very well when using the invocation method syntax, because methods such as the Product<T1,T2>(T1 p1, T2 p2) method can accept generic type parameters on their operands. Unfortunately, there is no way to generalize operators, and there is no way for a type of type AreaUnit<T> where T:LengthUnitDescriptor to define a conversion tool to or from some other arbitrary generic type AreaUnit<U> . AreaUnit<T> can determine conversions from, for example, AreaUnit<Angstrom> , but the compiler cannot say that the code provided by AreaUnit<Centimeters> and wants AreaUnit` can convert inches to angstroms and then to centimeters.

0
source

Source: https://habr.com/ru/post/899802/