Estructura de datos para acceder a unidades de medida

17

TL; DR : estoy tratando de diseñar una estructura de datos óptima para definir unidades dentro de una unidad de medida.


A Unit of measurees esencialmente una value(o cantidad) asociada con a unit. Las unidades SI tienen siete bases o dimensiones. A saber: longitud, masa, tiempo, corriente eléctrica, temperatura, cantidad de sustancia (moles) e intensidad luminosa.

Esto sería bastante sencillo, pero hay una serie de unidades derivadas, así como tasas que usamos con frecuencia. Una unidad combinada de ejemplo sería el Newton: kg * m / s^2y una tasa de ejemplo sería tons / hr.

Tenemos una aplicación que depende en gran medida de las unidades implícitas. Incorporaremos las unidades dentro del nombre de la variable o columna. Pero esto crea problemas cuando necesitamos especificar una unidad de medida con diferentes unidades. Sí, podemos convertir los valores en la entrada y en la pantalla, pero esto genera muchos códigos generales que nos gustaría encapsular dentro de su propia clase.

Existen varias soluciones en codeplex y otros entornos colaborativos. La licencia para los proyectos es aceptable, pero el proyecto en sí mismo generalmente termina siendo demasiado ligero o demasiado pesado. Estamos persiguiendo a nuestro propio unicornio de "justo".

Idealmente, podría definir una nueva unidad de medida usando algo como esto:

UOM myUom1 = nueva UOM (10, voltios);
UOM myUom2 = nueva UOM (43.2, Newtons);

Por supuesto, utilizamos una combinación de unidades imperiales y SI basadas en las necesidades de nuestros clientes.

También necesitamos mantener esta estructura de unidades sincronizadas con una tabla de base de datos futura para que podamos proporcionar el mismo grado de consistencia dentro de nuestros datos también.


¿Cuál es la mejor manera de definir las unidades, las unidades derivadas y las tasas que necesitamos usar para crear nuestra clase de unidad de medida? Pude ver el uso de una o más enumeraciones, pero eso podría ser frustrante para otros desarrolladores. Una sola enumeración sería enorme con más de 200 entradas, mientras que varias enumeraciones podrían ser confusas en función de las unidades SI frente a las imperiales y un desglose adicional basado en la categorización de la unidad en sí.

Ejemplos de Enum que muestran algunas de mis preocupaciones:

myUnits.Volt
myUnits.Newton
myUnits.meter

SIUnit.meter
ImpUnit.foot DrvdUnit.Newton
DrvdUnitSI.Newton
DrvdUnitImp.FtLbs

Nuestro conjunto de unidades en uso está bastante bien definido y es un espacio finito. Necesitamos la capacidad de expandir y agregar nuevas unidades derivadas o tasas cuando tengamos la demanda de los clientes. El proyecto está en C #, aunque creo que los aspectos de diseño más amplios son aplicables a múltiples lenguajes.


Una de las bibliotecas que miré permite la entrada de unidades de forma libre a través de una cadena. Su clase de UOM luego analizó la cadena y las cosas ranuradas en consecuencia. El desafío con este enfoque es que obliga al desarrollador a pensar y recordar cuáles son los formatos de cadena correctos. Y corro el riesgo de un error / excepción de tiempo de ejecución si no agregamos verificaciones adicionales dentro del código para validar las cadenas que se pasan en el constructor.

Otra biblioteca esencialmente creó demasiadas clases con las que el desarrollador tendría que trabajar. Junto con un equivalente UOM proporcionó una DerivedUnity RateUnitetcétera. Esencialmente, el código era demasiado complejo para los problemas que estamos resolviendo. Esa biblioteca esencialmente permitiría cualquiera: cualquier combinación (que es legítima en el mundo de las unidades), pero nos complace analizar nuestro problema (simplificar nuestro código) al no permitir todas las combinaciones posibles.

Otras bibliotecas eran ridículamente simples y ni siquiera habían considerado la sobrecarga del operador, por ejemplo.

Además, no estoy tan preocupado por los intentos de conversiones incorrectas (por ejemplo: voltios a metros). Los desarrolladores son los únicos que tendrán acceso a este nivel en este momento y no necesariamente tenemos que protegernos contra ese tipo de errores.


fuente
¿Podría explicar de qué manera las bibliotecas que encontró no satisfacen sus necesidades?
svick
1
Ver también stackoverflow.com/q/348853/240613
Arseni Mourzenko
1
@MainMa - gracias por ese enlace. No necesitamos hacer análisis dimensionales ya que nuestro espacio de problemas es lo suficientemente pequeño como para declarar las conversiones permitidas. Será un trabajo difícil de generar, pero es un costo único.
1
¿Puedes explicar qué tipo de conversiones necesitas? ¿Es solo una conversión de escala (por ejemplo, metro a centímetro) o también conversiones transversales (por ejemplo, masa a fuerza)?
Bart van Ingen Schenau
1
¿Has considerado mover parte del código a F #? Ese lenguaje tiene unidades de medida build int.
Pete

Respuestas:

11

Las bibliotecas Boost para C ++ incluyen un artículo sobre análisis dimensional que presenta una implementación de muestra de unidades de medida de manejo.

Para resumir: las unidades de medida se representan como vectores, y cada elemento del vector representa una dimensión fundamental:

typedef int dimension[7]; // m  l  t  ...
dimension const mass      = {1, 0, 0, 0, 0, 0, 0};
dimension const length    = {0, 1, 0, 0, 0, 0, 0};
dimension const time      = {0, 0, 1, 0, 0, 0, 0};

Las unidades derivadas son combinaciones de estos. Por ejemplo, la fuerza (masa * distancia / tiempo ^ 2) se representaría como

dimension const force  = {1, 1, -2, 0, 0, 0, 0};

Las unidades imperiales versus SI podrían manejarse agregando un factor de conversión.

Esta implementación se basa en técnicas específicas de C ++ (usando metaprogramación de plantillas para convertir fácilmente diferentes unidades de medida en diferentes tipos de tiempo de compilación), pero los conceptos deben transferirse a otros lenguajes de programación.

Josh Kelley
fuente
Entonces, ¿todas las unidades derivadas son equivalentes a const de C ++? ¿Supongo que están envueltos en un espacio de nombres para evitar contaminar cosas?
1
@ GlenH7: esto entra en el material de metaprogramación de la plantilla. En realidad, se representan como tipos separados (por ejemplo, mpl::vector_c<int,1,0,0,0,0,0,0>) en lugar de consts; El artículo presenta el enfoque de los concursos primero a modo de explicación (y probablemente no lo expliqué bien). El uso de consts funcionaría como una alternativa (perderías algo de seguridad de tipo de tiempo de compilación). Usar un espacio de nombres para evitar la contaminación de nombres es ciertamente una opción.
Josh Kelley
8

Acabo de lanzar Units.NET en Github y en NuGet .

Te da todas las unidades y conversiones comunes. Es liviano, probado en unidades y compatible con PCL.

Hacia su pregunta:

  • Esto está en el extremo más ligero de las implementaciones. El enfoque es ayudar en la representación, conversión y construcción inequívocas de unidades de medida.
  • No existe un solucionador de ecuaciones, no deriva automáticamente nuevas unidades de los cálculos.
  • Una gran enumeración para definir unidades.
  • Clase UnitConverter para convertir dinámicamente entre unidades.
  • Estructuras de datos inmutables para convertir explícitamente entre unidades.
  • Operadores sobrecargados para aritmética simple.
  • Extender a nuevas unidades y conversiones es una cuestión de agregar una nueva enumeración para la conversión dinámica y agregar una unidad de clase de medida, como Longitud, para definir propiedades de conversión explícitas y sobrecarga del operador.

Todavía tengo que ver el santo grial de soluciones en este dominio. Como usted dice, puede fácilmente volverse demasiado complejo o demasiado detallado para trabajar. A veces, es mejor mantener las cosas simples y para mis necesidades, este enfoque ha demostrado ser suficiente.

Conversión explícita

Length meter = Length.FromMeters(1);
double cm = meter.Centimeters; // 100
double yards = meter.Yards; // 1.09361
double feet = meter.Feet; // 3.28084
double inches = meter.Inches; // 39.3701

Pressure p = Pressure.FromPascal(1);
double kpa = p.KiloPascals; // 1000
double bar = p.Bars; // 1 × 10-5
double atm = p.Atmosphere; // 9.86923267 × 10-6
double psi = p.Psi; // 1.45037738 × 10-4

Conversión dinámica

// Explicitly
double m = UnitConverter.Convert(1, Unit.Kilometer, Unit.Meter); // 1000
double mi = UnitConverter.Convert(1, Unit.Kilometer, Unit.Mile); // 0.621371
double yds = UnitConverter.Convert(1, Unit.Meter, Unit.Yard); // 1.09361

// Or implicitly.
UnitValue val = GetUnknownValueAndUnit();

// Returns false if conversion was not possible.
double cm;
val.TryConvert(LengthUnit.Centimeter, out cm);
angularsen
fuente
Su ejemplo esencialmente parece utilizar unTruple<T1, T2, T3>(x, y, z)
Chef_Code
No estoy seguro de lo que quiere decir, solo hay un único valor almacenado para cada unidad. Para Longitud tiene un campo de metros de tipo doble, y para Masa es kilogramos. Al convertir a otras unidades, ejecuta ese valor a través de una función de conversión. Estas muestras están un poco desactualizadas ahora, pero se aplica el mismo concepto.
angularsen
Supongo que hablé mal y salté a conclusiones ... quiero decir Tuple. No puedo ver tu UnitConverterclase, pero en mi opinión, parece que puede compartir una funcionalidad similar a la Tupleclase.
Chef_Code
Todavía no estoy seguro acerca de la comparación de Tuple, pero vea la página de github para obtener ejemplos actualizados sobre el uso.
angularsen
3

Si puede cambiar a F # en su lugar usando C #, F # tiene un sistema de unidades de medida (implementado usando metadatos en los valores) que parece que se ajustará a lo que está tratando de hacer:

http://en.wikibooks.org/wiki/F_Sharp_Programming/Units_of_Measure

Notablemente:

// Additionally, we can define types measures which are derived from existing measures as well:

[<Measure>] type m                  (* meter *)
[<Measure>] type s                  (* second *)
[<Measure>] type kg                 (* kilogram *)
[<Measure>] type N = (kg * m)/(s^2) (* Newtons *)
[<Measure>] type Pa = N/(m^2)       (* Pascals *)
Pablo
fuente
Buena sugerencia, y lo hemos considerado. Sin embargo, no creo que F # nos dé la capacidad de controlar cómo las unidades terminan siendo mostradas en la salida.
2
@ GlenH7 Te creo en lo cierto:Important: Units of measure look like a data type, but they aren't. .NET's type system does not support the behaviors that units of measure have, such as being able to square, divide, or raise datatypes to powers. This functionality is provided by the F# static type checker at compile time, **but units are erased from compiled code**. Consequently, it is not possible to determine value's unit at runtime.
Paul
3

Basado en el hecho de que todas las conversiones requeridas son conversiones de escala (excepto si tiene que soportar conversiones de temperatura. Los cálculos donde la conversión implica un desplazamiento son significativamente más complejos), diseñaría mi sistema de 'unidad de medida' de esta manera:

  • Una clase que unitcontiene un factor de escala, una cadena para la representación textual de la unidad y una referencia a la que se unitescala. La representación textual está allí para fines de visualización y la referencia a la unidad base para saber en qué unidad se encuentra el resultado al hacer cálculos matemáticos de valores con diferentes unidades.

    Para cada unidad compatible, se proporciona una instancia estática de la unitclase.

  • Una clase que UOMcontiene un valor y una referencia a los valores unit. La UOMclase proporciona operadores sobrecargados para sumar / restar otro UOMy para multiplicar / dividir con un valor adimensional.

    Si la suma / resta se realiza en dos UOMcon el mismo unit, se realiza directamente. De lo contrario, ambos valores se convierten a sus respectivas unidades base y se suman / restan. El resultado se informa como estar en la base unit.

El uso sería como

unit volts = new unit(1, "V"); // base-unit is self
unit Newtons = new unit(1, "N"); // base-unit is self
unit kiloNewtons = new unit(1000, "kN", Newtons);
//...
UOM myUom1 = new UOM(10, volts);
UOM myUom2 = new UOM(43.2, kiloNewtons);

Como las operaciones en unidades incompatibles no se consideran un problema, no he tratado de hacer que el diseño sea seguro en ese sentido. Es posible agregar una verificación de tiempo de ejecución al verificar que dos unidades se refieren a la misma unidad base.

Bart van Ingen Schenau
fuente
Desde que mencionaste la temperatura: ¿qué es 95F - 85F? ¿Qué es 20C - 15C? En ambos ejemplos, ambos UOMs tendrían lo mismo unit. ¿Se realizarían las restas directamente?
@MattFenwick: Los resultados serían respectivamente 10 Fy 5 C. Los cálculos se realizan directamente si es posible, para evitar conversiones innecesarias. Sería bastante trivial agregar métodos de conversión de unidades UOM, pero para la conversión Celsius-Fahrenheit, la unitclase debería extenderse con la posibilidad de un desplazamiento además de un factor de escala.
Bart van Ingen Schenau
Pero 95F - 85F! = 10F.
1
@MattFenwick: Por favor, ilumíneme. Qué frío hace que llegar si se baja una temperatura de 95Fpor 85F? Que yo sepa, Fahrenheit sigue siendo una escala lineal.
Bart van Ingen Schenau
2
Hagamos el ejemplo de Celsius porque es más fácil convertir a Kelvin: si decimos 20C - 15C = 5C, entonces decimos 293.15K - 288.15K = 278.15K, lo cual es claramente incorrecto.
2

Piensa en lo que está haciendo tu código y lo que permitirá. Tener una enumeración simple con todas las unidades posibles en él me permite hacer algo como convertir voltios a metros. Obviamente, eso no es válido para un humano, pero el software lo intentará con gusto.

Hice algo vagamente similar a esto una vez, y mi implementación tenía clases base abstractas (longitud, peso, etc.) que todas implementaron IUnitOfMeasure. Cada clase de bases abstractas definió un tipo predeterminado (la clase Lengthtenía una implementación predeterminada de la clase Meter) que usaría para todo el trabajo de conversión. Por lo tanto, IUnitOfMeasureimplementó dos métodos diferentes, ToDefault(decimal)y FromDefault(decimal).

El número real que quería ajustar era un tipo genérico que aceptaba IUnitOfMeasurecomo argumento genérico. Decir algo así Measurement<Meter>(2.0)te da seguridad de tipo automático. Implementar las conversiones implícitas y los métodos matemáticos adecuados en estas clases le permite hacer cosas como Measurement<Meter>(2.0) * Measurement<Inch>(12)y devolver un resultado en el tipo predeterminado ( Meter). Nunca trabajé unidades derivadas como Newtons; Simplemente los dejé como kilogramo * metro / segundo / segundo.

mgw854
fuente
Me gusta el enfoque que sugieres con el uso de tipos genéricos.
1

Creo que la respuesta se encuentra en la respuesta de Stack Overflow de MarioVW a:

Ejemplo práctico ¿Dónde se puede usar Tuple en .Net 4-0?

Con las tuplas, podría implementar fácilmente un diccionario bidimensional (o n-dimensional para el caso). Por ejemplo, podría usar dicho diccionario para implementar una asignación de cambio de moneda:

var forex = new Dictionary<Tuple<string, string>, decimal>();
forex.Add(Tuple.Create("USD", "EUR"), 0.74850m); // 1 USD = 0.74850 EUR
forex.Add(Tuple.Create("USD", "GBP"), 0.64128m);
forex.Add(Tuple.Create("EUR", "USD"), 1.33635m);
forex.Add(Tuple.Create("EUR", "GBP"), 0.85677m);
forex.Add(Tuple.Create("GBP", "USD"), 1.55938m);
forex.Add(Tuple.Create("GBP", "EUR"), 1.16717m);
forex.Add(Tuple.Create("USD", "USD"), 1.00000m);
forex.Add(Tuple.Create("EUR", "EUR"), 1.00000m);
forex.Add(Tuple.Create("GBP", "GBP"), 1.00000m);
decimal result;
result = 35.0m * forex[Tuple.Create("USD", "EUR")]; // USD 35.00 = EUR 26.20
result = 35.0m * forex[Tuple.Create("EUR", "GBP")]; // EUR 35.00 = GBP 29.99
result = 35.0m * forex[Tuple.Create("GBP", "USD")]; // GBP 35.00 = USD 54.58

Tenía una necesidad similar para mi aplicación. TupleTambién es inmutable, lo que también es cierto para los objetos como Pesos y Medidas ... Como dice el refrán "un litro por libra al mundo".

Chef_Code
fuente
0

Mi código de prototipo: http://ideone.com/x7hz7i

Mis puntos de diseño:

  1. Elección de UM (Unidad de medida) como propiedad get / set
    Longitud len = nueva Longitud ();
    len.Meters = 2.0;
    Console.WriteLine (len.Feet);
    
  2. Nombrado constructor para elegir UM
    Longitud len = Length.FromMeters (2.0);
    
  3. Soporte ToString para UoM
    Console.WriteLine (len.ToString ("ft"));
    Console.WriteLine (len.ToString ("F15"));
    Console.WriteLine (len.ToString ("ftF15"));
    
  4. Conversión de ida y vuelta (pérdida de redondeo insignificante permitida por doble precisión)
    Length lenRT = Length.FromMeters (Length.FromFeet (Length.FromMeters (len.Meters) .Feet) .Meters);
    
  5. Sobrecarga del operador (pero sin verificación de tipo dimensional)
    // Bastante desordenado, con errores, inseguro y podría no ser posible sin usar F # o C ++ MPL.
    // Continúa diciendo que el Análisis Dimensional no es una característica opcional para UoM -
    // si lo usas directamente o no. Se requiere .
    
rwong
fuente
0

Hay un buen artículo en una revista que se encuentra en alemán: http://www.dotnetpro.de/articles/onlinearticle1398.aspx

La idea base es tener una clase de Unidad como Longitud con una Medición Base. La clase contiene el factor de conversión, las sobrecargas del operador, las sobrecargas de ToString, el analizador de cadenas y una implementación como indexador. Incluso hemos implementado incluso la vista Architectual, pero no se publica como una biblioteca.

public class Length : MeasurementBase
    {
        protected static double[] LengthFactors = { 1, 100, 1000, 0.001, 100 / 2.54 };
        protected static string[] LengthSymbols = { "m", "cm", "mm", "km", "in" };
...
      public virtual double this[Units unit]
        {
            get { return BaseValue * LengthFactors[(int)unit]; }
            set { BaseValue = value / LengthFactors[(int)unit]; }
        }
...

        public static ForceDividedByLength operator *(Length length, Pressure pressure1)
        {
            return new ForceDividedByLength(pressure1[Pressure.Units.kNm2] * length[Units.m], ForceDividedByLength.Units.kNm);
        }

...

Entonces puede ver el uso con el operador de Presión o simplemente:

var l = new Length(5, Length.Units.m)    
Area a = l * new Length("5 m");
a.ToString() // -> 25 m^2
double l2 = l[Length.Units.ft];

Pero como dijiste, tampoco encontré el unicornio :)

KCT
fuente
-1

Esta es la razón de ser del unitscomando Unix , que lo hace todo utilizando un enfoque basado en archivos de datos para especificar las relaciones.

Ross Patterson
fuente
Gracias por mencionar que units. La razón principal por la que las unidades no funcionarán para mi solución más amplia son las cadenas de forma libre. Por supuesto, proporciona mensajes de error a cambio, pero este enfoque está dirigido a desarrolladores que integrarán este código con nuestra aplicación. Las cadenas de forma libre presentan demasiadas oportunidades de error.
1
Deberías echar un vistazo al unitsarchivo de datos de. La forma en que define las relaciones entre cantidades es muy clara y puede ser útil para su problema.
Ross Patterson