Units of measure in C# - almost
Solution 1
You are missing dimensional analysis. For example (from the answer you linked to), in F# you can do this:
let g = 9.8<m/s^2>
and it will generate a new unit of acceleration, derived from meters and seconds (you can actually do the same thing in C++ using templates).
In C#, it is possible to do dimensional analysis at runtime, but it adds overhead and doesn't give you the benefit of compile-time checking. As far as I know there's no way to do full compile-time units in C#.
Whether it's worth doing depends on the application of course, but for many scientific applications, it's definitely a good idea. I don't know of any existing libraries for .NET, but they probably exist.
If you are interested in how to do it at runtime, the idea is that each value has a scalar value and integers representing the power of each basic unit.
class Unit
{
double scalar;
int kg;
int m;
int s;
// ... for each basic unit
public Unit(double scalar, int kg, int m, int s)
{
this.scalar = scalar;
this.kg = kg;
this.m = m;
this.s = s;
...
}
// For addition/subtraction, exponents must match
public static Unit operator +(Unit first, Unit second)
{
if (UnitsAreCompatible(first, second))
{
return new Unit(
first.scalar + second.scalar,
first.kg,
first.m,
first.s,
...
);
}
else
{
throw new Exception("Units must match for addition");
}
}
// For multiplication/division, add/subtract the exponents
public static Unit operator *(Unit first, Unit second)
{
return new Unit(
first.scalar * second.scalar,
first.kg + second.kg,
first.m + second.m,
first.s + second.s,
...
);
}
public static bool UnitsAreCompatible(Unit first, Unit second)
{
return
first.kg == second.kg &&
first.m == second.m &&
first.s == second.s
...;
}
}
If you don't allow the user to change the value of the units (a good idea anyways), you could add subclasses for common units:
class Speed : Unit
{
public Speed(double x) : base(x, 0, 1, -1, ...); // m/s => m^1 * s^-1
{
}
}
class Acceleration : Unit
{
public Acceleration(double x) : base(x, 0, 1, -2, ...); // m/s^2 => m^1 * s^-2
{
}
}
You could also define more specific operators on the derived types to avoid checking for compatible units on common types.
Solution 2
Using separate classes for different units of the same measure (e.g., cm, mm, and ft for Length) seems kind of weird. Based on the .NET Framework's DateTime and TimeSpan classes, I would expect something like this:
Length length = Length.FromMillimeters(n1);
decimal lengthInFeet = length.Feet;
Length length2 = length.AddFeet(n2);
Length length3 = length + Length.FromMeters(n3);
Solution 3
You could add extension methods on numeric types to generate measures. It'd feel a bit DSL-like:
var mass = 1.Kilogram();
var length = (1.2).Kilometres();
It's not really .NET convention and might not be the most discoverable feature, so perhaps you'd add them in a devoted namespace for people who like them, as well as offering more conventional construction methods.
Solution 4
I recently 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.
Example conversions:
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
Solution 5
Now such a C# library exists: http://www.codeproject.com/Articles/413750/Units-of-Measure-Validator-for-Csharp
It has almost the same features as F#'s unit compile time validation, but for C#. The core is a MSBuild task, which parses the code and looking for validations.
The unit information are stored in comments and attributes.
tina Miller
Saved from the web by a job in industry. Coding GUI for computer-aided railway realignment machinery. At last a job where I have time to do stuff properly and don't have to worry about loads of unfixed bugs coming back to haunt me.
Updated on July 08, 2022Comments
-
tina Miller almost 2 years
Inspired by Units of Measure in F#, and despite asserting (here) that you couldn't do it in C#, I had an idea the other day which I've been playing around with.
namespace UnitsOfMeasure { public interface IUnit { } public static class Length { public interface ILength : IUnit { } public class m : ILength { } public class mm : ILength { } public class ft : ILength { } } public class Mass { public interface IMass : IUnit { } public class kg : IMass { } public class g : IMass { } public class lb : IMass { } } public class UnitDouble<T> where T : IUnit { public readonly double Value; public UnitDouble(double value) { Value = value; } public static UnitDouble<T> operator +(UnitDouble<T> first, UnitDouble<T> second) { return new UnitDouble<T>(first.Value + second.Value); } //TODO: minus operator/equality } }
Example usage:
var a = new UnitDouble<Length.m>(3.1); var b = new UnitDouble<Length.m>(4.9); var d = new UnitDouble<Mass.kg>(3.4); Console.WriteLine((a + b).Value); //Console.WriteLine((a + c).Value); <-- Compiler says no
The next step is trying to implement conversions (snippet):
public interface IUnit { double toBase { get; } } public static class Length { public interface ILength : IUnit { } public class m : ILength { public double toBase { get { return 1.0;} } } public class mm : ILength { public double toBase { get { return 1000.0; } } } public class ft : ILength { public double toBase { get { return 0.3048; } } } public static UnitDouble<R> Convert<T, R>(UnitDouble<T> input) where T : ILength, new() where R : ILength, new() { double mult = (new T() as IUnit).toBase; double div = (new R() as IUnit).toBase; return new UnitDouble<R>(input.Value * mult / div); } }
(I would have liked to avoid instantiating objects by using static, but as we all know you can't declare a static method in an interface) You can then do this:
var e = Length.Convert<Length.mm, Length.m>(c); var f = Length.Convert<Length.mm, Mass.kg>(d); <-- but not this
Obviously, there is a gaping hole in this, compared to F# Units of measure (I'll let you work it out).
Oh, the question is: what do you think of this? Is it worth using? Has someone else already done better?
UPDATE for people interested in this subject area, here is a link to a paper from 1997 discussing a different kind of solution (not specifically for C#)
-
tina Miller over 15 yearsYeh, I knew that was what was missing, I quite like your solution, but as you say, it's not compile time. Vote up anyways.
-
tina Miller over 13 yearsNot sure why you got a downvote, but I think you'd probably be better off learning F# than trying to re-invent the wheel. I've updated my question with a link to a paper that might interest you.
-
John Alexiou over 13 yearsThanks for the constructive comment.
-
Chris Kerekes over 12 yearsThis was my first instinct too. The downside with this is that you have to explicitely define operators that combine all permutations of units. This gets much more complicated when you start combining different units together like Velocity (Length / TimeSpan) where you hyave a very large number of FromXXX conversions you would need to support.
-
Igor Pashchuk over 12 yearsthe link is bamboo.github.com/2008/08/05/…
-
justin.m.chase over 12 yearsfixed it, sorry I didn't see this sooner.
-
Brian almost 12 yearsI don't love seeing initializers which grow in complexity when we add more basic units. Since you are already losing the ability to detect wrong units at compile time, you could a step further and just use a dictionary mapping a string or enumeration to int rather than having a separate field for each type.
-
GKS over 11 yearsThere are only 7 base units if you take the SI system (time, mass, length, temperature, luminous intensity, substance amount and electrical current). If you add a multiplier value to Unit which is the conversion factory back to the SI representation you can get a fairly good model.
-
kmote over 10 yearsinteresting effort, but the author admits he is dissatisfied with the project and suggests starting again from scratch. A similar library can be found here: github.com/InitialForce/UnitsNet
-
Dinoel Vokiniv over 8 yearsYes, later comer to this discussion, but this is why I think this should best be a core language feature to assist developers rather than a code library, so that there are no abstractions remaining in the compiled code itself.
-
tina Miller almost 3 yearsWow! This was a trip down memory lane. Thanks for the interesting feedback. I recently had a similar problem that I solved (not very elegantly) with types - vector calculations for data from sources with different coordinate systems - I ended up with PointXY, PointYZ and PointXZ. Not pretty, but revealed several bugs.
-
Stelios Adamantidis almost 3 years@Benjol Haha, I hope that lane had good memories :) True it could be prettier but indeed it's good enough if it already revealed some bugs.