Structure de données pour accéder aux unités de mesure


17

TL; DR - J'essaie de concevoir une structure de données optimale pour définir les unités au sein d'une unité de mesure.


A Unit of measureest essentiellement une value(ou quantité) associée à a unit. Les unités SI ont sept bases ou dimensions. À savoir: longueur, masse, temps, courant électrique, température, quantité de substance (moles) et intensité lumineuse.

Ce serait assez simple, mais il existe un certain nombre d'unités dérivées ainsi que des taux que nous utilisons fréquemment. Un exemple d'unité combinée serait le Newton: kg * m / s^2et un exemple de taux serait tons / hr.

Nous avons une application qui dépend fortement des unités implicites. Nous incorporons les unités dans le nom de la variable ou de la colonne. Mais cela crée des problèmes lorsque nous devons spécifier une unité de mesure avec différentes unités. Oui, nous pouvons convertir les valeurs en entrée et afficher mais cela génère beaucoup de code de surcharge que nous aimerions encapsuler dans sa propre classe.

Il existe un certain nombre de solutions sur le codeplex et d'autres environnements collaboratifs. Les licences pour les projets sont agréables mais le projet lui-même finit généralement par être trop léger ou trop lourd. Nous chassons notre propre licorne de "juste ce qu'il faut".

Idéalement, je pourrais définir une nouvelle unité de mesure en utilisant quelque chose comme ceci:

UOM myUom1 = nouvelle UOM (10, volts);
UOM myUom2 = nouvelle UOM (43.2, Newtons);

Bien sûr, nous utilisons un mélange d'unités impériales et SI en fonction des besoins de nos clients.

Nous devons également garder cette structure d'unités synchronisée avec une future table de base de données afin que nous puissions également fournir le même degré de cohérence dans nos données.


Quelle est la meilleure façon de définir les unités, les unités dérivées et les taux que nous devons utiliser pour créer notre classe d'unités de mesure? Je pouvais voir utiliser une ou plusieurs énumérations, mais cela pourrait être frustrant pour d'autres développeurs. Une énumération unique serait énorme avec plus de 200 entrées, tandis que plusieurs énumérations pourraient être source de confusion en fonction des unités SI vs impériales et d'une ventilation supplémentaire en fonction de la catégorisation de l'unité elle-même.

Enum exemples montrant certaines de mes préoccupations:

myUnits.Volt
myUnits.Newton
myUnits.meter

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

Notre ensemble d'unités utilisées est assez bien défini et c'est un espace fini. Nous avons besoin de pouvoir étendre et ajouter de nouvelles unités ou taux dérivés lorsque nous avons la demande des clients pour eux. Le projet est en C # bien que je pense que les aspects de conception plus larges sont applicables à plusieurs langues.


L'une des bibliothèques que j'ai examinées permet la saisie sous forme libre d'unités via une chaîne. Leur classe UOM a ensuite analysé la chaîne et placé les choses en conséquence. Le défi de cette approche est qu'elle oblige le développeur à réfléchir et à se souvenir des formats de chaîne corrects. Et je court le risque d'une erreur / exception d'exécution si nous n'ajoutons pas de vérifications supplémentaires dans le code pour valider les chaînes transmises dans le constructeur.

Une autre bibliothèque a essentiellement créé trop de classes avec lesquelles le développeur devrait travailler. En plus d'un UOM équivalent , il a fourni un DerivedUnitet RateUnitet ainsi de suite. Essentiellement, le code était trop complexe pour les problèmes que nous résolvons. Cette bibliothèque permettrait essentiellement n'importe quelle: n'importe quelle combinaison (ce qui est légitime dans le monde des unités) mais nous sommes heureux d'étendre notre problème (simplifier notre code) en n'autorisant pas toutes les combinaisons possibles.

D'autres bibliothèques étaient ridiculement simples et n'avaient même pas envisagé la surcharge de l'opérateur par exemple.

De plus, je ne suis pas aussi inquiet des tentatives de conversions incorrectes (par exemple: volts en mètres). Les développeurs sont les seuls à accéder à ce niveau à ce stade et nous n'avons pas nécessairement besoin de nous protéger contre ce type d'erreurs.


Pourriez-vous expliquer en quoi les bibliothèques que vous avez trouvées ne correspondent pas à vos besoins?
svick


1
@MainMa - merci pour ce lien. Nous n'avons pas besoin de faire d'analyse dimensionnelle car notre espace de problème est suffisamment petit pour que nous déclarions simplement les conversions autorisées. Ce sera un slog à générer mais c'est un coût unique.

1
Pouvez-vous expliquer de quel type de conversions vous avez besoin? S'agit-il uniquement d'une conversion d'échelle (par exemple, de mètre en centimètre) ou également de conversions inter-dimensionnelles (par exemple de masse en force)?
Bart van Ingen Schenau

1
Avez-vous envisagé de déplacer une partie du code vers F #? Ce langage a des unités de mesure construites int.
Pete

Réponses:


11

Les bibliothèques Boost pour C ++ incluent un article sur l'analyse dimensionnelle qui présente un exemple d'implémentation de la gestion des unités de mesure.

Pour résumer: Les unités de mesure sont représentées comme des vecteurs, chaque élément du vecteur représentant une dimension fondamentale:

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

Les unités dérivées sont des combinaisons de celles-ci. Par exemple, la force (masse * distance / temps ^ 2) serait représentée comme

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

Les unités impériales par rapport aux unités SI peuvent être traitées en ajoutant un facteur de conversion.

Cette implémentation repose sur des techniques spécifiques au C ++ (en utilisant la métaprogrammation de modèle pour transformer facilement différentes unités de mesure en différents types de compilation), mais les concepts devraient être transférés vers d'autres langages de programmation.


Donc, toutes les unités dérivées sont équivalentes aux consts C ++? Je suppose qu'ils sont enveloppés dans un espace de noms pour éviter de polluer les choses?

1
@ GlenH7 - Cela entre dans le truc de métaprogrammation de modèle. Ils sont en fait représentés comme des types distincts (par exemple, mpl::vector_c<int,1,0,0,0,0,0,0>) au lieu de consts; l'article présente d'abord l'approche consts à titre d'explication (et je n'ai probablement pas bien expliqué cela). L'utilisation de consts fonctionnerait comme une alternative (vous perdriez une certaine sécurité de type à la compilation). L'utilisation d'un espace de noms pour éviter la pollution par les noms est certainement une option.
Josh Kelley

8

Je viens de publier Units.NET sur Github et sur NuGet .

Il vous donne toutes les unités et conversions courantes. Il est léger, testé à l'unité et prend en charge PCL.

Vers votre question:

  • C'est à l'extrémité plus légère des implémentations. L'accent est mis sur la représentation, la conversion et la construction sans ambiguïté d'unités de mesure.
  • Il n'y a pas de solveur d'équation, il ne dérive pas automatiquement de nouvelles unités des calculs.
  • Une grande énumération pour définir les unités.
  • Classe UnitConverter pour convertir dynamiquement entre les unités.
  • Structures de données immuables pour la conversion explicite entre les unités.
  • Opérateurs surchargés pour une arithmétique simple.
  • L'extension à de nouvelles unités et conversions consiste à ajouter une nouvelle énumération pour la conversion dynamique et à ajouter une unité de classe de mesure, comme la longueur, pour définir des propriétés de conversion explicites et une surcharge d'opérateur.

Je n'ai pas encore vu le Saint Graal des solutions dans ce domaine. Comme vous le dites, cela peut facilement devenir trop complexe ou trop verbeux pour être utilisé. Parfois, il vaut mieux garder les choses simples et pour mes besoins, cette approche s'est avérée suffisante.

Conversion explicite

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

Conversion dynamique

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

Votre exemple semble essentiellement utiliser unTruple<T1, T2, T3>(x, y, z)
Chef_Code

Vous ne savez pas ce que vous voulez dire, il n'y a qu'une seule valeur stockée pour chaque unité. Pour la longueur, il contient un champ de mètres de type double et pour la masse, il s'agit de kilogrammes. Lors de la conversion vers d'autres unités, il exécute cette valeur via une fonction de conversion. Ces échantillons sont un peu dépassés maintenant, mais le même concept s'applique.
angularsen

Je suppose que je me suis mal exprimé ainsi que j'ai tiré des conclusions ... Je voulais dire Tuple. Je ne peux pas voir votre UnitConverterclasse, mais il semble que l'OMI partage une fonctionnalité similaire à celle de la Tupleclasse.
Chef_Code

Vous n'êtes toujours pas sûr de la comparaison Tuple, mais consultez la page github pour des exemples mis à jour sur l'utilisation.
angularsen

3

Si vous pouvez tirer le passage à F # à la place en utilisant C #, F # a un système d'unités de mesure (implémenté à l'aide de métadonnées sur les valeurs) qui semble correspondre à ce que vous essayez de faire:

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

Notamment:

// 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 *)

Bonne suggestion, et nous l'avons étudiée. Je ne pense pas que F # nous donne la possibilité de contrôler comment les unités finissent par être affichées sur la sortie.

2
@ GlenH7 Je crois que vous avez raison: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

Sur la base du fait que toutes les conversions requises sont des conversions d'échelle (sauf si vous devez prendre en charge les conversions de température. Les calculs où la conversion implique un décalage sont beaucoup plus complexes), je concevoir mon système d'unité de mesure comme ceci:

  • Une classe unitcontenant un facteur d'échelle, une chaîne de représentation textuelle de l'unité et une référence que launit échelle. La représentation textuelle est là à des fins d'affichage et la référence à l'unité de base pour savoir dans quelle unité se trouve le résultat lors du calcul des valeurs avec des unités différentes.

    Pour chaque unité prise en charge, une instance statique de la unitclasse est fournie.

  • Une classe UOMcontenant une valeur et une référence à la valeur unit. La UOMclasse fournit des opérateurs surchargés pour ajouter / soustraire un autre UOMet pour multiplier / diviser avec une valeur sans dimension.

    Si l'addition / soustraction est effectuée sur deux UOMavec le même unit, elle est effectuée directement. Sinon, les deux valeurs sont converties en leurs unités de base respectives et ajoutées / soustraites. Le résultat est signalé comme étant dans la base unit.

L'utilisation serait comme

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

Comme les opérations sur des unités incompatibles ne sont pas considérées comme un problème, je n'ai pas essayé de sécuriser le type de conception à cet égard. Il est possible d'ajouter un contrôle d'exécution en vérifiant que deux unités se réfèrent à la même unité de base.


Puisque vous avez mentionné la température: qu'est-ce que c'est 95F - 85F? Qu'est-ce que c'est 20C - 15C? Dans les deux exemples, les deux UOMs auraient la même chose unit. Les soustractions seraient-elles effectuées directement?

@MattFenwick: Les résultats seraient respectivement 10 Fet 5 C. Les calculs sont effectués directement si possible, pour éviter les conversions inutiles. Il serait assez trivial d'ajouter des méthodes de conversion d'unité à UOM, mais pour la conversion Celsius-Fahrenheit, la unitclasse devrait être étendue avec la possibilité d'un décalage en plus d'un facteur d'échelle.
Bart van Ingen Schenau

Mais 95F - 85F! = 10F.

1
@MattFenwick: Veuillez m'éclairer. Comment fait-il froid si vous abaissez une température de 95Fprès 85F? À ma connaissance, Fahrenheit est toujours une échelle linéaire.
Bart van Ingen Schenau

2
Faisons l'exemple Celsius car il est plus facile de se convertir en Kelvin: si nous disons 20C - 15C = 5C, alors nous disons 293.15K - 288.15K = 278.15K, ce qui est clairement faux.

2

Réfléchissez à ce que fait votre code et à ce qu'il permettra. Avoir une énumération simple avec toutes les unités possibles me permet de faire quelque chose comme convertir des Volts en mètres. Ce n'est évidemment pas valable pour un humain, mais le logiciel se fera un plaisir d'essayer.

J'ai fait quelque chose de vaguement similaire à cela une fois, et mon implémentation avait des classes de base abstraites (longueur, poids, etc.) que toutes implémentaient IUnitOfMeasure. Chaque classe de bases abstraites a défini un type par défaut (la classe Lengthavait une implémentation par défaut de classe Meter) qu'elle utiliserait pour tous les travaux de conversion. D'où la IUnitOfMeasuremise en œuvre de deux méthodes différentes, ToDefault(decimal)et FromDefault(decimal).

Le nombre réel que je voulais encapsuler était un type générique acceptant IUnitOfMeasurecomme argument générique. Dire quelque chose comme Measurement<Meter>(2.0)vous donne une sécurité de type automatique. L'implémentation des conversions implicites et des méthodes mathématiques appropriées sur ces classes vous permet de faire des choses comme Measurement<Meter>(2.0) * Measurement<Inch>(12)et de retourner un résultat dans le type par défaut ( Meter). Je n'ai jamais travaillé sur des unités dérivées comme Newtons; Je les ai simplement laissés en kilogrammes * mètre / seconde / seconde.


J'aime l'approche que vous proposez avec l'utilisation de types génériques.

1

Je pense que la réponse réside dans la réponse de MarioVW Stack Overflow à:

Exemple pratique où Tuple peut être utilisé dans .Net 4-0?

Avec les tuples, vous pouvez facilement implémenter un dictionnaire bidimensionnel (ou n-dimensionnel d'ailleurs). Par exemple, vous pouvez utiliser un tel dictionnaire pour implémenter un mappage de change:

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

J'avais un besoin similaire pour ma candidature. Tupleest également immuable, ce qui est également vrai pour des objets tels que les poids et mesures… Comme le dit le proverbe "une pinte une livre dans le monde entier".


0

Mon code prototype: http://ideone.com/x7hz7i

Mes points de design:

  1. Choix de UoM (Unit of Measure) en tant que propriété get / set
    Longueur len = nouvelle longueur ();
    len.Meters = 2.0;
    Console.WriteLine (len.Feet);
    
  2. Constructeur nommé pour le choix de l'UoM
    Longueur len = Length.FromMeters (2.0);
    
  3. Prise en charge de ToString pour UoM
    Console.WriteLine (len.ToString ("ft"));
    Console.WriteLine (len.ToString ("F15"));
    Console.WriteLine (len.ToString ("ftF15"));
    
  4. Conversion aller-retour (perte d'arrondi négligeable permise par la double précision)
    Longueur lenRT = Length.FromMeters (Length.FromFeet (Length.FromMeters (len.Meters) .Feet) .Meters);
    
  5. Surcharge de l'opérateur (mais manquante vérification du type dimensionnel)
    // Assez désordonné, bogué, dangereux et pourrait ne pas être possible sans utiliser F # ou C ++ MPL.
    // Il poursuit en disant que l' analyse dimensionnelle n'est pas une fonctionnalité facultative pour UoM -
    // que vous l'utilisiez directement ou non. C'est obligatoire .
    

0

Il y a un bon article dans un magazine sans que ce soit en allemand: http://www.dotnetpro.de/articles/onlinearticle1398.aspx

L'idée de base est d'avoir une classe d'unité comme la longueur avec une mesure de base. La classe contient le facteur de conversion, les surcharges d'opérateur, les surcharges ToString, l'analyseur de chaînes et une implémentation en tant qu'indexeur. Nous avons même implémenté la vue architecturale, mais elle n'est pas publiée en tant que bibliothèque.

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

...

Vous voyez donc l'utilisation avec l'opérateur de pression ou simplement:

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

Mais comme vous l'avez dit, je n'ai pas non plus trouvé la licorne :)


-1

C'est la raison d'être de la unitscommande Unix , qui fait tout en utilisant une approche basée sur un fichier de données pour spécifier les relations.


Merci d'avoir mentionné units. La principale raison pour laquelle les unités ne fonctionneront pas pour ma solution plus large est les chaînes de forme libre. Certes, il fournit des messages d'erreur en retour, mais cette approche cible les développeurs qui intégreront ce code à notre application. Les chaînes de forme libre présentent trop de risques d'erreur.

1
Vous devriez jeter un œil au unitsfichier de données de. La façon dont il définit les relations entre les quantités est très propre et peut être utile à votre problème.
Ross Patterson
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.