Click here to Skip to main content
Click here to Skip to main content

Units of Measure Validator for C#

By , 2 Jul 2012
Rate this:
Please Sign up or sign in to vote.

Introduction

In C#, the sum of 1 meter and 1 second will be 2. But in most cases, adding or assigning two quantities of different units is a mistake of the programmer, which causes an unexpected and undesired runtime behavior.

This validator checks at compile time for unit violations without adding a new syntax or changing the runtime behavior - only the used units has to be specified through comments or attributes.

Figure 1: Validation Sample

Considering the units in figure 1, planet.Speed is meter per second, resultingForce is Newton (kg * m/s^2) and planet.Mass is kilogram. Since the quotient of Newton and kilogram is an acceleration, this quantity cannot be assigned to a speed - a mistake of the programmer, but detected even before starting the whole application.

Usage

The validator is available as MSBuild task and can be included inside any cs-project, whereas a reference at runtime is not needed. The units can be declared within a single project through xml documentation, but for project wide consistence, it is recommend to use the Unit-attribute. In this case, a runtime reference is needed to HDUnitsOfMeasureLibrary.

Installation

First, download either the binaries of this library or compile the library by yourself. All these assemblies should be placed into a suitable folder. 

If you want to install the validator for all projects (not recommended, and you should know what you are doing), you can edit the Microsoft.CSharp.Targets file, otherwise you can install it for each project separately by editing its csproj-file. To do this, open the file with an editor and replace the following comment: 

<!--
<Target Name="BeforeBuild">
</Target>
-->

with this XML:

<UsingTask TaskName="HDLibrary.UnitsOfMeasure.Validator.UnitValidationTask"
    AssemblyFile="Path-To-Your-Installation-Folder\HDUnitsOfMeasureValidatorTask.dll" />
<Target Name="BeforeBuild" DependsOnTargets="ResolveProjectReferences;ResolveAssemblyReferences">
  <UnitValidationTask Files="@(Compile)" References="@(_ResolveAssemblyReferenceResolvedFiles)" />
</Target>

You should adapt "Path-To-Your-Installation-Folder" to the path of your folder in which you placed your copy of HDUnitsOfMeasureValidatorTask.dll.

That is it - the next time you will build your project, unit violations are displayed within Visual Studio!

First Steps 

By default, the unit of each variable or member is unknown. The behavior of unknown units is the same as without validation. Thus, to work with this validator, the physical elements (like speed, length or time) should be marked with their corresponding units.

Only operations on those marked elements are evaluated.

Declaring Units

For Local Variables

You can declare a unit for a local variable by adding an one-line comment which encloses the unit in square brackets. Between the comment token (//) and the opening bracket, no characters are allowed. After the closing bracket, an explaining text can be added, separated by a whitespace. 

Examples:

int length; //[m] the length in meter
double time; //[s]

For Properties and Fields 

The units of properties and fields can be declared through a xml documentation: 

/// <summary>
/// The length in [m]
/// </summary>
private double length;

/// <summary>
/// The time in [s]
/// </summary>
private double Time {get; set;} 

The unit can be specified anywhere inside the summary-tag and has to be enclosed in square brackets. 

It is of disadvantage, that the unit description is only available for references within this project, because references from other projects currently have no access to the xml documentation and assume the unit of this member as "unknown". 

For this case, you can use the UnitAttribute. Here it is of disadvantage, that you have to add an assembly reference to HDUnitsOfMeasureLibrary.dll, and this dependency still exists at runtime. Nevertheless, using the Unit-attribute is the recommended way:

[Unit("m")]
private double length;

[Unit("s")]
private double Time {get; set;}

For Methods, Constructors and Parameters 

Methods and constructors are parameterized members, which have several input units and zero or one output unit (for constructors, the output unit, if given, determines the unit of the created object). 

If you want to declare the unit within the xml documentation, you can do it the following way: 

/// <param name="time">time ([s])</param>
/// <param name="number">number in [1] - number is a dimensionless quantity</param>
/// <param name="unknown">can be any unit, no unit is specified</param>
/// <returns>the length, [m]</returns>
public int GetLength(int time, int number, double unknown) {...} 

If no unit is specified for any of the parameters, its unit is unknown. 

Unknown Units in contrast to Dimensionless Units 

If no unit is specified, the unit is unknown. Unknown units are equal to dimensionless units, but assigns to unknown quantities are not validated:

double number = 1; //[1] dimensionless unit
double value = 1; //unknown unit
double length = 1.Meter(); //[m] a length in meters

number = value; //is ok
value = number; //is ok
value = length; //is ok, since no validation is done
number = length; //is not ok, since number is "1" and length is "m"

The Vicious Circle of Units 

Since the unit of a hard coded number is dimensionless, you cannot assign it to an element with a different unit than dimensionless. To break this circle, you can insert the comment "/*ignore unit*/" (no whitespaces) exactly after the "=" in the assignment - such assignments are not validated. 

With the predefined constants in the Units class or the extension methods on int and double, you can easily convert any unknown or dimensionless unit into any target unit without using "ignore unit".

int time = 5; //[m] -> not ok, since 5 is unknown ("1") and time is "m"
time =/*ignore unit*/5; -> is ok, since this assignment will not be validated
time = 5 * Units.Meter; //is ok, since Units.Meter is 1 meter
time = 5.Meter(); //is ok
time = 5.AsUnit("m"); //is ok
time = 5.AsUnit("km").ConvertFromTo("km", "m"); //is ok

Validation

The following assignments to elements with a known unit are validated:
=, *=, /=, +=, -=

Arguments in method invocations are validated, if the parameters require certain units.

A validation is successful, if the inferred unit from the expression is equal to the unit of the target element. Two units are equal if they have the same coherent unit (see here, chapter "What are coherent Units?") and the same conversion factor to it. So, "km" is not equal to "m", but "MN" (Mega newton) is equal to "Gg * m / s^2" (Giga gram * meter / square-second).

Dynamic Unit Descriptions

Usually, each unit description is static. If "a" is declared as meter, "a" will be always meter.

But this is not true for class members or methods: If a vector is declared as meter, the length of the vector is meter too. If a vector is declared as Newton, the length of the vector will be Newton.

This genericity can be accomplished by dynamic unit descriptions. Those descriptions can be resolved into concrete units by involving the used context (the target object and the arguments). Such a dynamic unit can be specified with the DynamicUnitAttribute.

There are three kinds of dynamic expressions: The "@" is the only one which can be used with properties and fields. If you declare the length of a vector as "@", the unit of the target object will be inserted. So if the vector is declared as X, its length is X too. You can even build some more complex expressions like "m/@" or "@^2" - depending on the context it will be "m/m" or "m^2" (if the target object is declared as "m"). 

With curly brackets, the unit of the enclosed parameter can be referenced. Square brackets will insert the value of the argument passed to the referenced parameter. If the referenced parameter is a string and has the attribute UnitDescriptionAttribute, the validator will check this argument to be a valid unit string, if the argument is a constant expression. This attribute can define an underlaying unit constraint for this string. If the underlaying unit constraint is "m", valid arguments are "km" or "cm". This constraint can be dynamic, too.

A parameter can be referenced by its zero based index or its name. 

As example, here the signature of the ConvertFromTo method:

[return: DynamicUnit("[targetUnitString]")]
public static double ConvertFromTo([DynamicUnit("[sourceUnitString]")] this double value,
	[UnitString] string sourceUnitString, [UnitString(UnderlayingUnitConstraint = "[sourceUnitString]")] string targetUnitString)

The result unit is the value of the targetUnitString argument. targetUnitString has to be a valid unit string, which has to be the same underlying unit as the sourceUnitString argument.

If value is kilometer, sourceUnitString has to be "km", otherwise the validator logs an error. If sourceUnitString is "km", targetUnitString has to be some kind of length too - e.g. "cm" or "µm".

External Unit Definition and Description 

If you want to introduce a new unit, you can use the assembly attribute UnitDefinitionAttribute

[assembly: UnitDefinition("N", "Newton", "kg * m/s^2")]

This unit is then available for this project and all projects, which reference this project.

If you want to describe the units of an foreign element, you can use the attribute UnitDescriptionAttribute:

[assembly: UnitDescription("System.Windows.Vector.Length", ResultUnit = "@")]

Beside the result unit, you can define the parameter units for parameterized members too. In this case, you can even specify the overloading for this description (e.g. "double,double").

An example:

[assembly: UnitDescription("System.Math.Min", ResultUnit = "{0}", ParameterUnits = ",{0}")]

This description forces the second argument to have the same unit as the first. At the same time it specifies the unit of the result. The first argument can be any unit.

Thus, the resulting unit of Math.Min(1.Meter(), 2.Meter()) will be meter. 

Implementation, a Short Overview

The core libraries of this validator are NRefactory and my HDUnitsOfMeasureLibrary.

The process of validation is separated into different steps, some of them are parallelized.

First, the code is parsed into an abstract syntax tree. Parallel to this, the referenced assemblies are loaded (if not cached) using Mono.Cecil. Then both are converted into a single type-system. This type-system is traversed to find references to the same element and to extract the units from the attributes and xml documentation. After this step, each element has an unit description. An unit description can resolve possible dynamic units into concrete units by analyzing the passed target object and arguments. 

In the next step, the whole abstract syntax tree is traversed again (but now parallelized) to validate assignments and method calls by using the extracted units and the inferred units of expressions. 

Future 

This library is still under development - there are still some bugs and limitations.

Currently, one big issue is the performance: Despite parallelization, it is still quite slow - but there are enough points to optimize.

History 

  • 1.0 Initial version 

License

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

About the Author

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

Comments and Discussions

 
QuestionGread idea, but I can't get it to work properly. PinmemberMember 959045511-Nov-12 5:52 
GeneralRe: Gread idea, but I can't get it to work properly. PinmemberHenning Dieterichs11-Nov-12 10:57 
QuestionAssign unit definition to items of list PinmemberThecentury3-Sep-12 22:52 
BugNull Reference Exception on build PinmemberThecentury3-Sep-12 22:37 
GeneralMy vote of 5 PinmemberJean Paul V.A13-Jul-12 13:48 
GeneralMy vote of 5 PinmemberShlomiO11-Jul-12 9:22 
GeneralMy vote of 4 PinmemberDerndorfer11-Jul-12 1:09 
GeneralRe: My vote of 4 [modified] PinmemberHenning Dieterichs11-Jul-12 1:26 
GeneralRe: My vote of 4 PinmemberDerndorfer11-Jul-12 1:34 
GeneralRe: My vote of 4 PinmemberHenning Dieterichs11-Jul-12 2:12 
GeneralMy vote of 5 PinmemberGalatei9-Jul-12 9:07 
GeneralMy vote of 5 Pinmemberpita20123-Jul-12 8:48 
QuestionValidation vs New Types PinmvpPaulo Zemek3-Jul-12 8:04 
GeneralRe: Validation vs New Types PinmemberHenning Dieterichs3-Jul-12 12:29 
GeneralRe: Validation vs New Types PinmvpPaulo Zemek3-Jul-12 12:34 
GeneralRe: Validation vs New Types PinmemberHenning Dieterichs4-Jul-12 1:29 
GeneralRe: Validation vs New Types PinmemberHenning Dieterichs5-Jul-12 11:31 
GeneralRe: Validation vs New Types PinmvpPaulo Zemek5-Jul-12 11:55 
GeneralRe: Validation vs New Types [modified] PinmemberQwertie10-Jul-12 10:24 
GeneralRe: Validation vs New Types PinmemberHenning Dieterichs10-Jul-12 11:15 
GeneralRe: Validation vs New Types PinmemberQwertie10-Jul-12 19:55 
GeneralRe: Validation vs New Types [modified] PinmemberQwertie11-Jul-12 4:14 
GeneralRe: Validation vs New Types PinmemberHenning Dieterichs11-Jul-12 11:48 
Questionnice PinmemberCIDev3-Jul-12 7:31 
GeneralMy vote of 5 Pinmemberashved3-Jul-12 4:13 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.140415.2 | Last Updated 2 Jul 2012
Article Copyright 2012 by Henning Dieterichs
Everything else Copyright © CodeProject, 1999-2014
Terms of Use
Layout: fixed | fluid