21

TL;DR - I'm trying to design an optimal data structure to define units within a unit of measure.


A Unit of measure is essentially a value (or quantity) associated with a unit. SI Units have seven bases or dimensions. Namely: length, mass, time, electric current, temperature, amount of substance (moles), and luminous intensity.

This would be straightforward enough, but there are a number of derived units as well as rates that we frequently use. An example combined unit would be the Newton: kg * m / s^2 and an example rate would be tons / hr.

We have an application that relies heavily upon implied units. We'll embed the units within the variable or column name. But this creates problems when we need to specify a unit of measure with different units. Yes, we can convert the values at input and display but this generates a lot of overhead code that we'd like to encapsulate within its own class.

There are a number of solutions out on codeplex and other collaborative environments. The licensing for the projects is agreeable but the project itself usually ends up being too lightweight or too heavy. We're chasing our own unicorn of "just right."

Ideally, I could define a new unit of measure using something like this:

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

Of course, we use a mix of Imperial and SI units based upon our clients' needs.

We also need to keep this structure of units synchronized with a future database table so we can provide the same degree of consistency within our data too.


What's the best way of defining the units, derived units, and rates that we need to use to create our unit of measure class? I could see using one or more enums, but that could be frustrating for other developers. A single enum would be huge with 200+ entries whereas multiple enums could be confusing based upon SI vs Imperial units and additional breakdown based upon categorization of the unit itself.

Enum examples showing some of my concerns:

myUnits.Volt
myUnits.Newton
myUnits.meter

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

Our set of units in use is pretty well defined and it's a finite space. We do need the ability to expand and add new derived units or rates when we have client demand for them. The project is in C# although I think the broader design aspects are applicable to multiple languages.


One of the libraries I looked at allows for free-form input of units via string. Their UOM class then parsed the string and slotted things accordingly. The challenge with this approach is that it forces the developer to think and remember what the correct string formats are. And I run the risk of a runtime error / exception if we don't add additional checks within the code to validate the strings being passed in the constructor.

Another library essentially created too many classes that the developer would have to work with. Along with an equivalent UOM it provided a DerivedUnit and RateUnit and so on. Essentially, the code was overly complex for the problems we're solving. That library would essentially allow any:any combinations (which is legitimate in the units world) but we're happy to scope our issue (simplify our code) by not allowing every possible combination.

Other libraries were ridiculously simple and hadn't even considered operator overloading for example.

In addition, I'm not as worried about attempts at incorrect conversions (for example: volts to meters). Devs are the only ones who will access at this level at this point and we don't necessarily need to protect against those types of mistakes.

9
  • Could you explain in what ways don't the libraries you found fit your needs?
    – svick
    Commented May 8, 2013 at 13:29
  • 1
    See also stackoverflow.com/q/348853/240613 Commented May 8, 2013 at 13:42
  • 1
    @MainMa - thanks for that link. We don't need to do dimensional analysis as our problem space is small enough that we would just declare the allowed conversions. It will be a slog to generate but that's a one time cost.
    – user53019
    Commented May 8, 2013 at 13:47
  • 1
    Can you explain what kind of conversions you need? Is it only scaling conversion (e.g. meter to centimeter) or also cross-dimensional conversions (e.g. mass to force)? Commented May 8, 2013 at 14:58
  • 1
    Have you considered moving part of the code to F#? That language has units of measure build int.
    – Pete
    Commented Oct 24, 2014 at 13:14

9 Answers 9

13

The Boost libraries for C++ include an article on dimensional analysis that presents a sample implementation of handling units of measure.

To summarize: Units of measurement are represented as vectors, with each element of the vector representing a fundamental dimension:

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

Derived units are combinations of these. For example, force (mass * distance / time^2) would be represented as

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

Imperial versus SI units could be handled by adding a conversion factor.

This implementation relies on C++-specific techniques (using template metaprogramming to easily turn different units of measurement into different compile-time types), but the concepts should transfer to other programming languages.

2
  • So all of the derived units are equivalent to C++ consts? I presume they're wrapped into a namespace to avoid polluting things?
    – user53019
    Commented May 8, 2013 at 13:42
  • 1
    @GlenH7 - This gets into the template metaprogramming stuff. They're actually represented as separate types (e.g., mpl::vector_c<int,1,0,0,0,0,0,0>) instead of consts; the article presents the consts approach first by way of explanation (and I probably didn't explain that well). Using consts would work as an alternative (you'd lose some compile-time type safety). Using a namespace to avoid name pollution is certainly an option. Commented May 8, 2013 at 13:46
8

I just released Units.NET on Github and on NuGet.

It gives you all the common units and conversions. It is light-weight, unit tested and supports PCL.

Towards your question:

  • This is on the lighter end of the implementations. Focus is to aid in unambiguous representation, conversion and construction of units of measurement.
  • There is no equation solver, it does not automatically derive new units from calculations.
  • One big enum for defining units.
  • UnitConverter class for converting dynamically between units.
  • Immutable data structures for converting explicitly between units.
  • Overloaded operators for simple arithmetic.
  • Extending to new units and conversions is a matter of adding a new enum for dynamic conversion and adding a unit of measurement class, like Length, for defining explicit conversion properties and operator overloading.

I have yet to see the holy grail of solutions in this domain. As you state, it can easily become way too complex or too verbose to work with. Sometimes, it's best to keep things simple and for my needs this approach has proven sufficient.

Explicit conversion

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

Dynamic conversion

// 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);
4
  • Your example essentially seems to to utilize a Truple<T1, T2, T3>(x, y, z)
    – Chef_Code
    Commented Feb 14, 2016 at 9:58
  • Not sure what you mean, there is just a single value stored for each unit. For Length it holds a meters field of type double, and for Mass it is kilograms. When converting to other units it runs that value through a conversion function. These samples are a bit outdated now, but the same concept applies.
    – angularsen
    Commented Feb 14, 2016 at 18:36
  • I guess I misspoke as well as jumped to conclusions... I meant Tuple. I can not see your UnitConverter class, but IMO it appears like it may share a similar functionality as the Tuple class.
    – Chef_Code
    Commented Feb 14, 2016 at 20:27
  • Still not sure about the Tuple comparison, but see the github page for updated examples on usage.
    – angularsen
    Commented Feb 14, 2016 at 21:00
3

If you can pull switching to F# instead using C#, F# has a units of measure system (implemented using metadata on values) that looks like it'll fit what you're trying to do:

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

Notably:

// 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 *)
2
  • Good suggestion, and we've considered it. I don't believe F# gives us the ability to control how the units end up being displayed on output though.
    – user53019
    Commented May 8, 2013 at 13:41
  • 2
    @GlenH7 I believe you're right: 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
    Commented May 8, 2013 at 13:45
3

Based on the fact that all required conversions are scaling conversions (except if you have to support temperature conversions. Calculations where the conversion involves an offset are significantly more complex), I would design my 'unit of measure' system like this:

  • A class unit containing a scaling factor, a string for the unit's textual representation and a reference that the unit scales to. The textual representation is there for display purposes and the reference to the base unit to know which unit the result is in when doing math on values with different units.

    For each supported unit, a static instance of the unit class is provided.

  • A class UOM containing a value and a reference to the value's unit. The UOM class provides overloaded operators for adding/subtracting another UOM and for multiplying/dividing with a dimensionless value.

    If addition/subtraction is performed on two UOM with the same unit, it is performed directly. Otherwise both values are converted to their respective base units and the added/subtracted. The result is reported as being in the base unit.

Usage would be like

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

As operations on incompatible units are not considered an issue, I have not tried to make the design type-safe in that respect. It is possible to add a runtime check by verifying that two units refer to the same base unit.

7
  • Since you mentioned temperature: what is 95F - 85F? What is 20C - 15C? In both examples, both UOMs would have the same unit. Would the subtractions be performed directly?
    – user39685
    Commented May 8, 2013 at 17:46
  • @MattFenwick: The results would be respectively 10 F and 5 C. The calculations are performed directly if possible, to avoid unneeded conversions. It would be fairly trivial to add unit-conversion methods to UOM, but for Celsius-Fahrenheit conversion, the unit class would have to be extended with the possibility of an offset in addition to a scaling factor. Commented May 9, 2013 at 11:21
  • But 95F - 85F != 10F.
    – user39685
    Commented May 9, 2013 at 12:03
  • 1
    @MattFenwick: Please enlighten me. How cold does it get if you lower a temperature of 95F by 85F? To my knowledge, Fahrenheit is still a linear scale. Commented May 9, 2013 at 12:21
  • 2
    Let's do the Celsius example because it's easier to convert to Kelvin: if we say 20C - 15C = 5C, then we're saying 293.15K - 288.15K = 278.15K, which is clearly wrong.
    – user39685
    Commented May 9, 2013 at 12:52
2

Think about what your code is doing, and what it will allow. Having a simple enumeration with all the possible units in it allows me to do something like convert Volts to meters. That's obviously not valid to a human, but the software will gladly try.

I did something vaguely similar to this once, and my implementation had abstract base classes (length, weight, etc.) that all implemented IUnitOfMeasure. Each abstract bases class defined a default type (class Length had a default implementation of class Meter) that it would use for all conversion work. Hence IUnitOfMeasure implemented two different methods, ToDefault(decimal) and FromDefault(decimal).

The actual number I wanted to wrap was a generic type accepting IUnitOfMeasure as its generic argument. Saying something like Measurement<Meter>(2.0) gives you automatic type safety. Implementing the proper implicit conversions and math methods on these classes allows you to do things like Measurement<Meter>(2.0) * Measurement<Inch>(12) and return a result in the default type (Meter). I never worked out derived units like Newtons; I simply left them as Kilogram * Meter / Second / Second.

1
  • I like the approach you're suggesting with the use of generic types.
    – user53019
    Commented May 8, 2013 at 13:43
1

I believe the answer lies in MarioVW's Stack Overflow response to:

Practical Example Where Tuple can be used in .Net 4-0?

With tuples you could easily implement a two-dimensional dictionary (or n-dimensional for that matter). For example, you could use such a dictionary to implement a currency exchange mapping:

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

I had a similar need for my application. Tuple is also immutable which is also true of objects such as Weights and Measures… As the saying goes "a pint a pound the world around."

0

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

My design points:

  1. Choice of UoM (Unit of Measure) as Property get/set
    Length len = new Length();
    len.Meters = 2.0;
    Console.WriteLine(len.Feet);
    
  2. Named constructor for choice of UoM
    Length len = Length.FromMeters(2.0);
    
  3. ToString support for UoM
    Console.WriteLine(len.ToString("ft"));
    Console.WriteLine(len.ToString("F15"));
    Console.WriteLine(len.ToString("ftF15"));
    
  4. Roundtrip conversion (negligible rounding loss permitted by double precision)
    Length lenRT = Length.FromMeters(Length.FromFeet(Length.FromMeters(len.Meters).Feet).Meters);
    
  5. Operator overloading (but lacking dimensional type check)
    // Quite messy, buggy, unsafe, and might not be possible without using F# or C++ MPL.
    // It goes on to say that Dimensional Analysis is not an optional feature for UoM -
    // whether you use it directly or not. It is required.
    
0

There is a good article in a magazin unfourtunatly it is in German: http://www.dotnetpro.de/articles/onlinearticle1398.aspx

Base idea is to have a Unit class like Length with a BaseMeasurement. The class contains the conversion factor, operator overloads, ToString overloads, parser of strings and an implementation as an indexer. We've even implemented even the Architectual view but it's not release as a library.

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

...

So you see the usage with the Pressure operator or just:

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

But as you said, I didn't find the unicorn either :)

-1

This is the raison d'etre of the Unix units command, which does it all using a data-file-driven approach for specifying the relationships.

2
  • Thank you for mentioning units. The main reason units won't work for my broader solution is the free form strings. Granted, it provides error messages in return, but this approach is targeting developers who will integrate this code with our application. Free form strings presents too much opportunity for error.
    – user53019
    Commented May 9, 2013 at 2:10
  • 1
    You should take a look at units's data file. The way it defines relationships between quantities is very clean, and might be useful to your problem. Commented May 9, 2013 at 10:36

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.