Click here to Skip to main content
15,867,141 members
Articles / DevOps / Unit Testing

Units of Measure Library for .NET

Rate me:
Please Sign up or sign in to vote.
4.93/5 (7 votes)
20 Jun 2012CPOL8 min read 59.5K   2K   35   18
This article introduces a library for handling units of measure.

Introduction

This library provides several classes to work with units of measure, and this as near as possible to the SI standard. It gives the ability to load unit definitions from XML, to parse units, transform them and do calculations with them. Derived units like "km/h" are supported and can be constructed dynamically by dividing the unit "km" through "h". Beside shifted units like "Fahrenheit" and "Celsius", even custom units can be added. The attached demo project demonstrates, how to import self-updating currency units from the Internet. Thus, you can convert "l/EUR" into "cm^3/USD" by using up-to-date currency rates.

Since the library itself is fully XML documented, I will confine myself to the essentials.

This library is part of the Units of Measure Validator for C#.

Concept of Units

There are three different types of units: Base units, scaled/shifted units and derived units.
Each unit has an optional name and an abbreviation. The abbreviation is required for units that are registered to an unit parser, and for base units, since the abbreviation defines the identity of a base unit.

Base units cannot be shortened or constructed from other units. An example is "Meter" or "Second". Two base units are equal, if their abbreviations are equal.

Scaled/Shifted units consists of a factor, an offset and an underlaying unit. An example is "Kilometer", which is 1000 "Meter". The underlaying unit can be any unit type, so you can define the unit "hour" as 60 minutes, and a minute as 60 seconds. Two scaled/shifted units are equal, if they have the same coherent unit and the same factor and offset in relation to this coherent unit. So "30 min" is equal to "half-an-hour", considering "min" is registered as "60 s" and "half-an-hour" as "0.5 hour", "30 min" or "1800 s" - all three options are possible. The offset is needed for unproportional units like degrees Fahrenheit or degrees Celsius. The coherent unit of Fahrenheit and Celsius is Kelvin.

Derived units are composed units with multiple parts, whereas each part consists of a unit of any type and an exponent. An example is "km/h", which is the same as "km^1 * h^-1" or "liter", which is the same as "dm^3". A derived unit is coherent, if all parts are coherent except for scaled/shifted unit parts: If a scaled/shifted unit is unproportional (i.e. an offset different from 0), its coherent unit is (but only for derived units) the most underlaying unit with the same offset. Thus, the coherent unit of cm/µ°C (centimeter per microcelsius) is m/°C, and not m/°K. This is, because a transformation from quantity a in m/°C to b in m/°K is not possible (see the chapter "Shifted Units in Connection with Derived Units").

A special case is the base unit Dimensionless unit, whose abbreviation is "1". A dimensionless unit is a neutral element, so any unit multiplied or divided by this special unit is the unit itself. The dimensionless unit divided by an other unit, is the reciprocal of the other unit (e.g. 1/m).

All units inherits from the Unit class, the dimensionless unit is represented by the static and read only Unit.Dimensionless property.

Units can be exponentiated, multiplied and divided using Unit.Pow(int), * and /.

Transformations

A QuantityTransformation describes a transformation for a value in one unit to another. Two transformations can be chained, if the target unit of the first transformation is equal to the source unit of second. Such a chained transformation describes a transformation from the very first unit to the very last.

With Reverse, each transformation can be reversed, so that the source unit and the target unit are exchanged. A transformation from meter to kilometer returns a transformation from kilometer to meter.

The LinearQuantityTransformation class is a QuantityTransformation and describes linear transformations like "Transform(value) = Factor * value + Offset".

Two transformations are equal, if their target units are equal and the mathematical transformation is the same. Two non-base units are equal, if their transformations to their coherent units are equal.

What are Coherent Units?

A coherent unit does not need any factors (or offsets) to express relationships to other coherent units. All base units are coherent.

For example, the definition of newton:

Image 1,

all these units are coherent, thus, newton is coherent too.

It would be possible to define 1 newton' as

Image 2 ,

but since gram is no coherent unit, this definition is not coherent, as well as definitions containing any factors like

Image 3 .

Nevertheless, it is possible to transform 1 newton' to the coherent newton by multiplying with 1000. This way, the unit of measure library can determine the equality between N' and N''.

Parsing Units

Several formats are supported through different unit parsers, each of them implements IUnitParser. If parsing fails, either null is returned or a format exception will be thrown, depending on the throwFormatException parameter.

Registered Unit Parser

This parser implements IUnitParser and IUnitRegistry and uses a string-unit dictionary to find the corresponding unit for a given string. "1" as dimensionless unit is registered by default.

Prefixed Unit Parser

This parser is capable of parsing strings like "km" or "THz" by going through all available prefixes in the Prefix class and delegating the parsing of the underlaying unit ("m" for "km" or "Hz" for "THz") to an other parser (e.g. the Registered Unit Parser).

Scaled Shifted Unit Parser

The Scaled Shifted Unit Parser parses strings consisting of a factor and a unit abbreviation (e.g. "60 s", the definition for 1 minute). The unit abbreviation is resolved through an injected IUnitParser. It is recommended to use a Composed Unit Parser, containing both a Registered Unit Parser and a Prefixed Unit Parser.

Derived Unit Parser

With this parser, unit expressions like "m^2 * s^-1 / kg^2 * K" can be parsed.

The whitespaces are optional, and if the exponent is equal to 1, the exponent is optional too. Every exponent right from "/" is inverted. Although it is mathematically not correct, it looks much clearer without the braces (unless you convince me of the contrary in your comments).

The parsing of abbreviations is delegated to an injected IUnitParser as for the Scaled Shifted Unit Parser. It is recommended too, to use a Composed Unit Parser, containing both a Registered Unit Parser and a Prefixed Unit Parser.

Composed Unit Parser

The composed unit parser aggregates multiple IUnitParser instances and returns the first successful result. This way, all the mentioned parsers can be combined into a single one.

The method GetUnitRegistry returns the first instance implementing the IUnitRegistry interface (or null if no such instance exists), so new units can be registered quickly into a composed unit parser.

The static method NewDefaultUnitParser returns a fully usable unit parser containing all parsers mentioned above.

CachedUnitParser

This parser delegates the parsing to an injected unit parser and caches the result (the cache will not be emptied automatically).

Loading Unit Definitions from XML

The class XmlUnitLibrary parses an XML document and loads the units into an IUnitRegistry. The XML document has to be compliant to a XSD schema which can be found in the source of the HDUnitsOfMeasure project.

Generally, the XML document looks like:

XML
<?xml version="1.0" encoding="utf-8" ?>
<UnitLibrary xmlns="http://www.hediet.de/xsd/unitlibrary/1.0">
  <BaseUnit Name="Kelvin" Abbr="K"/>
  <ScaledShiftedUnit Name="Celsius" Abbr="°C" 
  Factor="1" Offset="273.15" UnderlayingUnit="K" />
  <ScaledShiftedUnit Name="Celsius" Abbr="°F" 
  Factor="0.55555555555555555555" Offset="255.37222222222222222222" 
  UnderlayingUnit="K" />
  
  <DerivedUnit Name="Meters per Second" Abbr="mps">
    <UnitPart  Unit="m" Exponent="1" /> 
    <!-- the definition for m and s is left out in this snippet -->
    <UnitPart  Unit="s" Exponent="-1" />
  </DerivedUnit>
</UnitLibrary>

Threadsafety

In principle, using this library is thread safe, since each Unit is immutable or the access to members is synchronized. Parsing units is thread safe too as well as using the global unit parser.

Demo

The attached C# demo project demonstrates, how to use the default units and import self-updating currency units from the Internet. You can enter a value, a source unit and a target unit. If a conversion is possible, the result will be displayed.

Shifted Units in Connection with Derived Units

Generally, the transformation from one unit (x) to the underlaying (f) can be expressed by a function f(x).

Scaled/shifted units are described through

Image 4 .

A unit is unproportional, if its offset is different from 0. This becomes a problem as soon multiple units are combined.

Assuming g(z):

Image 5 .

If a value v is given in the unit x/z (1v = 1x/z), its transformation to v' in the unit f/g (1v' = 1f/g) can be described as:

Image 6 .

Only if x and z are given or offset is 0, v' can be calculated.

Assuming offset is zero (if the unit x is proportional):

Image 7 .

So if unproportional units are used in connection with derived units, they cannot be converted to their coherent equivalent. Nevertheless, µ°C (microcelsius) can be converted to °C, thus °C is the coherent unit for µ°C, if it is part of a derived unit (annotation: standalone, a transformation from µ°C to K is still possible).

For this purpose, the interface ICouldBeUnproportional has been introduced. Units which implement this interface, provides methods to get a proportional transformation to a "coherent" unit.

In principle, it is even not allowed to add two units with an offset:
1 °C + 1 °C = 274 K + 274 K = 548 K = 275 °C.

To solve this problem, you can use C° that describes a difference celsius:
1 °C + 1 C° = 274 K + 1 K = 275 K = 2 °C.
C° is an alias for Kelvin, thus a coherent unit too.

History

  • 1.1 Some updates
  • 1.0 Initial release

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Student
Germany Germany
Presently I am a student of computer science at the Karlsruhe Institute of Technology in Germany.

Comments and Discussions

 
QuestionThe library allows to add shifted unit values to base units Pin
MRusaev20-Oct-14 9:18
MRusaev20-Oct-14 9:18 
Questionvery nice Pin
BillW3312-Aug-14 9:30
professionalBillW3312-Aug-14 9:30 
GeneralMy vote of 5 Pin
Swab.Jat3-Feb-14 19:45
Swab.Jat3-Feb-14 19:45 
Questionfactor/offset determination Pin
tubias27-Mar-13 22:04
tubias27-Mar-13 22:04 
QuestionSimilarity with Martin Fowler's measurement model Pin
heltonbiker23-Feb-13 6:22
heltonbiker23-Feb-13 6:22 
Your impressive project reminded me immediately of Chapter 3 - Observations and Measurements of Martin Fowler's excellent book "Analysis Patterns, Reusable Object Models".

His concepts are:
- Quantity, which is a combination of (double)Amount and (Unit)Unit, containing also a (QuantityTransformation)ConversionRatio - to convert to some CoherentUnit;
- CompoundUnit, which is equivalent to DerivedUnits.

While Fowler's model is intended to be introductory and generalistic, it is very interesting to see a full implementation of such concepts in a Visual Studio project. It most probably will inspire my work in the next months.

Also, it is interesting to take a look at Fowler's concepts that go beyond the concept of Units of Measure, such as MeasurementProcess (which he calls simply Measure, as opposed to Quantity), PhenomenonType, Observation, Protocol, RejectedObservation, AssociatedObservation, etc. (this is all derived from the complex needs of medical observations, an area in which I work by the way).

Congrats for your nice work!
Questioncould not parse "мкм". . .why? Pin
SergeyAB24-Aug-12 0:29
SergeyAB24-Aug-12 0:29 
QuestionParsers Pin
flamen_x30-Jun-12 11:28
flamen_x30-Jun-12 11:28 
AnswerRe: Parsers Pin
Henning Dieterichs30-Jun-12 12:59
Henning Dieterichs30-Jun-12 12:59 
GeneralRe: Parsers Pin
flamen_x2-Jul-12 2:27
flamen_x2-Jul-12 2:27 
QuestionCelsisus & Kelvin Special Case? Pin
cjb11020-Jun-12 21:37
cjb11020-Jun-12 21:37 
AnswerRe: Celsisus & Kelvin Special Case? Pin
Henning Dieterichs20-Jun-12 22:18
Henning Dieterichs20-Jun-12 22:18 
GeneralRe: Celsisus & Kelvin Special Case? Pin
cjb11020-Jun-12 23:52
cjb11020-Jun-12 23:52 
GeneralRe: Celsisus & Kelvin Special Case? Pin
Henning Dieterichs21-Jun-12 0:21
Henning Dieterichs21-Jun-12 0:21 
GeneralRe: Celsisus & Kelvin Special Case? Pin
cjb11021-Jun-12 3:46
cjb11021-Jun-12 3:46 
QuestionCaution: doubles! Pin
Andreas Gieriet20-Jun-12 16:31
professionalAndreas Gieriet20-Jun-12 16:31 
AnswerRe: Caution: doubles! Pin
Henning Dieterichs20-Jun-12 19:43
Henning Dieterichs20-Jun-12 19:43 
GeneralRe: Caution: doubles! Pin
motorboy7931-Jul-12 9:48
motorboy7931-Jul-12 9:48 
GeneralRe: Caution: doubles! Pin
Henning Dieterichs2-Aug-12 5:33
Henning Dieterichs2-Aug-12 5:33 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.