volumen unidades tabla presion peso medidas masa longitud liquidas fisica convertidor conversor conversion c# .net generics delegates units-of-measurement

c# - tabla - convertidor de unidades de presion



¿Cómo creo un convertidor genérico para unidades de medida en C#? (6)

He estado tratando de aprender un poco más sobre delegados y lambdas mientras trabajo en un pequeño proyecto de cocina que involucra conversión de temperatura, así como algunas conversiones de medición de cocina como Imperial a Metric y he estado tratando de pensar en una forma de hacer una convertidor de unidad extensible.

Aquí es con lo que comencé, junto con los comentarios del código sobre algunos de mis planes. No tengo planes de usarlo como el siguiente, estaba probando algunas características de C #. No lo sé muy bien, tampoco estoy seguro de cómo llevar esto más allá. ¿Alguien tiene alguna sugerencia sobre cómo crear lo que estoy hablando en los comentarios a continuación? Gracias

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

Actualización: tratando de aclarar mi pregunta:

Usando solo mi ejemplo de temperatura a continuación, ¿cómo crearía una clase que contenga una lista de conversiones lambda a Celsius, que luego le pasará una temperatura determinada e intentará convertir eso a Celsius (si el cálculo está disponible)?

Ejemplo de pseudo código:

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


Parece que quieres algo como:

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;

Sin embargo, es posible que desee considerar no solo usar decimal , sino tener un tipo que conozca las unidades para que no pueda accidentalmente (digamos) aplicar la conversión "Celsius a Kelvin" a un valor que no sea Celsius. Posiblemente eche un vistazo al enfoque F # Unidades de medida para inspirarse.


Pensé que este era un pequeño problema interesante, así que decidí ver qué bien podría ser envuelto en una implementación genérica. Esto no está bien probado (y no maneja todos los casos de error, como por ejemplo, si no registra la conversión para un tipo de unidad en particular, luego la transfiere), pero podría ser útil. El foco estaba en hacer que la clase heredada ( TemperatureConverter ) fuera lo más ordenada posible.

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

Los argumentos arg genéricos son para una enumeración que representa las unidades y el tipo para el valor. Para usarlo, solo tiene que heredar de esta clase (proporcionando los tipos) y registrar algunas lambdas para hacer la conversión. Aquí hay un ejemplo de temperatura (con algunos cálculos simulados):

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

Y luego usarlo es bastante 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));


Tienes un buen comienzo, pero como dijo Jon, actualmente no es seguro para tipos; el convertidor no tiene comprobación de errores para garantizar que el decimal que obtiene sea un valor Celsius.

Entonces, para llevar esto más allá, comenzaría a introducir tipos de estructuras que toman el valor numérico y lo aplican a una unidad de medida. En los Patrones de la Arquitectura Empresarial (también conocido como la Pandilla de los cuatro patrones de diseño), esto se conoce como el patrón "Dinero" después del uso más común, para denotar una cantidad de un tipo de moneda. El patrón se cumple para cualquier cantidad numérica que requiera que una unidad de medida sea significativa.

Ejemplo:

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; } }

Ahora, tiene un tipo simple que puede usar para forzar que las conversiones que está realizando tomen una Temperatura que sea de la escala adecuada y devuelva un resultado de temperatura que se sabe que está en la otra escala.

Sus Funcs necesitarán una línea adicional para verificar que la entrada coincida con la salida; puedes seguir usando lambdas, o puedes dar un paso más con un patrón de estrategia simple:

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

Debido a que estos tipos de conversiones son bidireccionales, puede considerar configurar la interfaz para manejar ambas formas, con un método "ConvertBack" o similar que tomará una Temperatura en la escala Celsius y la convertirá en Fahrenheit. Eso reduce tu conteo de clases. Entonces, en lugar de instancias de clase, los valores de su diccionario podrían ser punteros a métodos en instancias de los convertidores. Esto aumenta un tanto la complejidad de configurar el selector de estrategia principal de TemperatureConverter, pero reduce el número de clases de estrategia de conversión que debe definir.

También tenga en cuenta que la comprobación de errores se realiza en tiempo de ejecución cuando realmente está tratando de realizar la conversión, lo que requiere que este código se pruebe minuciosamente en todos los usos para garantizar que siempre sea correcto. Para evitar esto, puede derivar la clase base de temperatura para producir estructuras CelsiusTemperature y FahrenheitTemperature, que simplemente definirían su Scale como un valor constante. Entonces, el ITemperatureConverter podría convertirse en genérico en dos tipos, ambas Temperaturas, lo que le permite comprobar en tiempo de compilación que está especificando la conversión que cree que es. el TemperatureConverter también se puede hacer para encontrar ITemperatureConverters dinámicamente, determinar los tipos entre los que se convertirá y configurar automágicamente el diccionario de convertidores para que nunca tenga que preocuparse por agregar otros nuevos. Esto tiene el costo de aumentar el recuento de clases basado en la temperatura; necesitarás cuatro clases de dominio (una base y tres clases derivadas) en lugar de una. También ralentizará la creación de una clase TemperatureConverter, ya que el código para construir reflexivamente el diccionario del convertidor usará un poco de reflexión.

También podría cambiar las enumeraciones para que las unidades de medida se conviertan en "clases de marcador"; clases vacías que no tienen otro significado que el de ser de esa clase y derivar de otras clases. A continuación, puede definir una jerarquía completa de clases "UnitOfMeasure" que representan las diversas unidades de medida, y se puede utilizar como argumentos y restricciones de tipo genérico; ITemperatureConverter podría ser genérico para dos tipos, ambos con restricciones para ser clases TemperatureScale, y una implementación CelsiusFahrenheitConverter cerraría la interfaz genérica a los tipos CelsiusDegrees y FahrenheitDegrees, ambos derivados de TemperatureScale. Eso le permite exponer las unidades de medida a sí mismas como restricciones de una conversión, lo que a su vez permite conversiones entre tipos de unidades de medida (ciertas unidades de ciertos materiales tienen conversiones conocidas; 1 British Imperial Pint of water pesa 1.25 libras).

Todas estas son decisiones de diseño que simplificarán un tipo de cambio en este diseño, pero a un costo (ya sea haciendo algo más difícil de hacer o disminuyendo el rendimiento del algoritmo). Depende de usted decidir qué es realmente "fácil" para usted, en el contexto del entorno general de aplicación y codificación en el que trabaja.

EDITAR: El uso que desee, de su edición, es extremadamente fácil para la temperatura. Sin embargo, si desea un UnitConverter genérico que pueda funcionar con cualquier UnitofMeasure, ya no desea que los Enums representen sus unidades de medida, porque los Enums no pueden tener una jerarquía de herencia personalizada (derivan directamente de System.Enum).

Puede especificar que el constructor predeterminado puede aceptar cualquier Enum, pero luego debe asegurarse de que Enum sea uno de los tipos que es una unidad de medida; de lo contrario, podría pasar un valor de DialogResult y el convertidor se volvería loco en tiempo de ejecución.

En cambio, si quiere un UnitConverter que pueda convertir a cualquier UnitOfMeasure dado lambdas para otras unidades de medida, yo especificaría las unidades de medida como "clases de marcador"; pequeños "tokens" sin estado que solo tienen un significado porque son de su propio tipo y derivan de sus padres:

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

Puede ingresar la verificación de errores (verifique que la unidad de entrada tenga una conversión especificada, verifique que una conversión agregada sea para una unidad de medida con el mismo elemento principal que el tipo base, etc.) como lo considere oportuno. También puede derivar UnitConverter para crear TemperatureConverter, lo que le permite agregar comprobaciones estáticas de tipo de tiempo de compilación y evitar las comprobaciones en tiempo de ejecución que UnitConverter debería usar.


Normalmente quería agregar esto como un comentario a la publicación de Danny Tuppeny, pero parece que no puedo agregar esto como comentario.

Mejoré un poco la solución de @Danny Tuppeny. No quería agregar cada conversión con dos factores de conversación, porque solo uno debería ser necesario. Además, el parámetro de tipo Func no parece ser necesario, solo lo hace más complicado para el usuario.

Entonces mi llamada se vería así:

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

También agregué un método para obtener el factor de conversión entre unidades. Esta es la clase modificada UnitConverter:

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


Se puede definir un tipo genérico de unidades físicas tal que, si para cada unidad tiene un tipo que implementa new e incluye un método de traducción entre esa unidad y una "unidad base" de ese tipo, se puede realizar una aritmética con valores expresados ​​en diferentes unidades y hacer que se conviertan según sea necesario, utilizando el sistema de tipo de modo que una variable de tipo AreaUnit<LengthUnit.Inches> solo acepte elementos acotados en pulgadas cuadradas, pero si uno dice myAreaInSquareInches= AreaUnit<LengthUnit.Inches>.Product(someLengthInCentimeters, someLengthInFathoms); automáticamente traduciría esas otras unidades antes de realizar la multiplicación. En realidad, puede funcionar bastante bien cuando se utiliza la sintaxis método-llamada, ya que métodos como el método del Product<T1,T2>(T1 p1, T2 p2) pueden aceptar parámetros de tipo genérico para sus operandos. Desafortunadamente, no hay forma de hacer genéricos a los operadores, ni hay forma de que un tipo como AreaUnit<T> where T:LengthUnitDescriptor defina un medio de conversión hacia o desde algún otro tipo genérico arbitrario AreaUnit<U> . Un AreaUnit<T> podría definir conversiones AreaUnit<Angstrom> desde, por ejemplo, AreaUnit<Angstrom> , pero no hay forma de que al compilador se le diga que el código al que se le da una AreaUnit<Centimeters> and wants AreaUnit` puede convertir pulgadas a angstroms y luego a centímetros.


Puedes echar un vistazo a Units.NET. Está en GitHub y NuGet . Proporciona la mayoría de las unidades y conversiones, admite el tipado estático y la enumeración de unidades y el análisis / impresión de abreviaturas. Sin embargo, no analiza las expresiones y no puede ampliar las clases existentes de unidades, pero puede ampliarlas con unidades nuevas de terceros.

Conversiones de ejemplo:

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