9
votes

I'm implementing value types which represents a distance (or length). There's an enum that represents the different units of measure, eg:

public enum DistanceUnit 
{
   Millimeter,
   Centimeter,
   Meter,
   Kilometer,
   Inch,
   Foot,
   Yard,
   Mile
};

These measurements fall into one of two systems - either metric or imperial. Since enum doesn't support hierachies, what's the best pattern for representing this hierarchy?

Is it to use bit flags? Or use two seperate enums along with some methods to correlate them? Or to declare static members instead of enums?

Give me some advice... how would you implement this?


Edit - More clarification: I have several immutable structs (generated by T4) which represent various measurements:

public struct Meters : IEquatable<Distance>, 
                       IEquatable<Meters>, 
                       IEquatable<Millimeters>, ... IComparable<> etc.. etc..
{
    public readonly decimal Value;
    ...
    public static implicit operator Distance (Meters other) {
        // Can implicitly cast to Distance
    }
}

public struct Millimeters ...
public struct Centimeters ....

... etc, as well as a hand-coded immutable Distance, intended to represent any measure:

public struct Distance : IEquatable<Distance>, 
                       IEquatable<Meters>, 
                       IEquatable<Millimeters>, ...
                       IFormattable
{

    public readonly decimal Value;
    public readonly DistanceUnits UnitOfMeasure;
    ...
    public string ToString(string format, IFormatProvider provider)
    {
        // Logic:
        // If UOM = Meters and Value < .1 then display as "10 cm" instead of ".1 M"
        // If UOM = Inches and value multiple of 12, display as feet
        // etc, etc
    }
}

Foregoing a discussion about whether the Distance should be converted to the correct UOM by calling code, the goal here is for ToString to convert the value up or down on the same measurement (imperial or metric) that is represented by the current UnitOfMeasure.

Obviously this could all be hard coded into the ToString method, but given that I'm also implementing TypeConverters and FormatProviders for this whole shibang, I'd like to find a generic means of figuring out, from a DistanceUnit, what the appropriate next-up or next-down unit of measure would be.

Am I barking up the wrong tree by wanting to implement in this manner?

3
I would use [Flags] as you suggest to implement the categories in the MSB bits and the low order to represent the exponent.kenny

3 Answers

10
votes

Do you really need an enum here? Maybe, a simple value object will do?

public class Distance
{
    private readonly decimal millimeters;

    public decimal Meters
    { 
        get { return millimeters * 0.001m; } 
    }

    private Distance(decimal millimeters)
    {
        this.millimeters = millimeters;
    }

    public static Distance Yards(decimal yards)
    {
        return new Distance(yards * 914.4m);
    }
}

With extension methods you and properly defined operators can get very Ruby-like syntax:

var theWholeNineYards = 9.Yards() + 34.Inches();
2
votes

Generally speaking, I would go with Anton's solution. But if your implementation can't use that, and you need things to be used similar to an enum, I think this is a natural way to use the units:

DistanceUnit.Metric.Millimeter  
DistanceUnit.Imperial.Inch  

In order to use it like that, there should be:

public static class DistanceUnit  
{
  public static MetricDistanceUnit Metric;
  public static ImperialDistanceUnit Imperial;
}   

Where MetricDistanceUnit is:

public enum MetricDistanceUnit  
{
   Millimeter, Centimeter ...
}

And ImperialDistanceUnit has the same structure..

1
votes

Maybe all you need is a function that returns a corresponding unit subset

class UnitSystem
{
  public enum Type
  {
    Metric,
    Imperial
  }

  public static DistanceUnit[] GetUnits(Type type)
  {
    switch (type)
    {
      case Type.Metric:
        return new DistanceUnit[] {
          DistanceUnit.Millimeter,
          DistanceUnit.Centimeter,
          DistanceUnit.Meter,
          DistanceUnit.Kilometer
        }

      case Type.Imperial:
        return new DistanceUnit[] {
          DistanceUnit.Inch,
          DistanceUnit.Foot,
          DistanceUnit.Yard,
          DistanceUnit.Mile
        }
    }
  }

  public static Type GetType(DistanceUnit unit)
  {
    switch (unit)
    {
      case DistanceUnit.Millimeter:
      case DistanceUnit.Centimeter:
      case DistanceUnit.Meter:
      case DistanceUnit.Kilometer:
        return Type.Metric;

      case DistanceUnit.Inch:
      case DistanceUnit.Foot:
      case DistanceUnit.Yard:
      case DistanceUnit.Mile:
        return Type.Imperial;
    }
  }
}