AddThis Social Bookmark Button

Print

.NET Localization, Part 4: Localizing Units

by Satya Komatineni and Elena Tulchinskaya
10/28/2002

Introduction

When a Web site is accessible by the international community, one of the considerations is how we present units of measure: Length, Width, Height, Weight, Area, Volume, etc. .NET provides some support by making available a RegionInfo class which identifies whether a locale is mks (metric) or fps (imperial). The goal of this article is to design a localization scheme for working with units of measure that can satisfy the following requirements:

  1. Display quantities with appropriate unit labels on Web pages
  2. Read quantities from entry fields by taking into account the locale of the client and the locale of the server
  3. Print text labels according to the language of the locale
  4. Contain a variety of units in the same measurement system. For example: pounds, tons, feet, miles.
  5. Programmers on the server side usually work with only one set of units, whether metric or imperial
  6. Region information should be hidden, as much as is possible, from the programmer

On the server side, we will choose either metric or imperial and all calculations are performed in those units. It is only for  display purposes that we convert the quantities between units, depending on the user's region.

Let us start with the design of teh basic classes involved and show you how basic OO principles make this process simple and logical.

Related Reading

.NET Framework Essentials
By Thuan L. Thai, Hoang Lam

Class Design: Units

At the root of this scheme there are two complementary concepts: a "Measure" and a "Unit". Measure is the quantity or the number value. Unit is its type; e.g., Miles.


public interface IUnit
{
   string   getName();        // name of the unit
   string   getShortLabel();  // short label
   string   getLongLabel();   // long label
   
   // Conversion from a standard unit   
   double    getConversionFactor();   
   
   // What is the comparable unit in the other
   IUnit   getMetricImperialEquivalent();   system
}

The interface IUnit stipulates that every unit that implements IUnit must have a name (Foot), a short label (ft), and a long label (Foot). A conversion factor for a unit represents the multiplication factor with respect to a standard unit. This conversion factor will allow us to convert from one unit to any other unit. The equivalent unit returned by the method getMetricImperialEquivalent() will allow us to find out the equivalent unit in the other system. For instance, mile and kilometer are equivalent, meaning they are comparable measures. This explanation should be evident when we examine the definition for a "Foot" as an example of unit of length:

  
public abstract class LengthUnit : IUnit {}
public abstract class WeightUnit : IUnit {}

public class Foot : LenghtUnit
{
  public static Foot self = new Foot();

  private Foot()
  {
  }
   
  public string getName() 
  { 
    return "Foot"; 
  }

  public string getShortLabel() 
  { 
    return "ft"; 
  }
  
  public string getLongLabel() 
  { 
    return "Feet"; 
  }
  
  public double getConversionFactor() 
  { 
    return 1; 
  }
  
  public IUnit getMetricImperialEquivalent()
  { 
    return Units.Meter.self;
  }
}

Having LengthUnit and WeightUnit will allow us the type safety in conversions. The term Units.Meter.self requires a bit of explanation. For example, if there is only one Meter object in the entire system, there is no need to have multiple objects representing that type. So this is a singleton with a private constructor. Anyone that refers to it uses the "self" reference pointing to the single instance. The assumption in Units.Meter.self or Units.Foot.self is that Units represent the namespace, which is not shown in these examples.

Class Design: Measures

An IMeasure represents the quantitative value of a measure, along with its unit. For example, "50Ft" is a measure which has two parts: the number 50 and the unit "Ft". The labels for a measure are derived from its underlying unit. The function getAs() converts a measure from one unit to another, whereas the function getValueAs() is a shortcut for getAs(), where it returns just the quantity part of the measure.


public interface IMeasure
{
  // What is the numerical value of the measure
  double       getValue();    

  // What is the unit of the above numerical
  IUnit      getUnit();

  // Language sensitive short label      
  string       getShortLabel();      

  // Language sensitive long label
  string       getLongLabel();      

  // Convert the number to a different unit
  IMeasure    getAs(IUnit convUnit);      

  // simplification of the above method 
  // just to get the value and not the unit.
  double       getValueAs(IUnit convUnit);    
}

getShortLabel and getLongLabel will simply ask its unit what those values are. A Measure is a good candidate for an abstract class, as we can implement a good portion of that interface leaving only a few essential details for the derived classes.


public abstract class AMeasure:IMeasure
{
  // place holder for the numerical value
  private double    m_value;    
  
  // corresponding unit object
  private IUnit    m_unit;      

  protected AMeasure(double value, IUnit unit)
  {
    m_value = value; m_unit = unit;
  }

  // Language sensitive short label   
  public string       getShortLabel()         
  {
    return m_unit.getShortLabel();
  }
  
  // Language sensitive long label
  public string       getLongLabel()         
  {
    return m_unit.getLongLabel();
  }
  // Let the derived classes implement this
  abstract IMeasure    getAs(IUnit convUnit);      

  // simplifacation of the above method 
  // just to get the value and not the unit.
  public double       getValueAs(IUnit convUnit)
  {
    IMeasure m = getAs(convUnit);
    return m.getValue();
  }
} // end of AMeasure

The function getAs will return the measure in a different unit. This allows for converting from say, meters to kilometers, pounds to tons, etc. Let us implement measure for Length to investigate the implementation details of derived classes:


public class Length  : AMeasure 
{
  public static LengthUnit standardUnit = Units.Foot.self;

  public Length(double value, LegthUnit unit)
    :base(value,unit){}
               
  public override IMeasure    getAs(IUnit targetUnit)      
  {
    // convert the source unit to standard unit
    // convert from the standard unit to the targetunit
    if (!targetUnit is LengthUnit)
    {
      throw Exception("Can only convert between length units");
    }

    double srcConversionFactor = 
                                getUnit().getConversionFactor();
    double targetConversionFactor = 
                               targetUnit.getConversionFactor();

    double srcValue = getValue();
    double targetValue = 
        srcValue * srcConversionFactor / targetConversionFactor;
      
    return new Length(targetValue,targetUnit);
  }
}

Using the above length example, it is not hard to imagine how one can write measures for weight, area, volume, etc. It is conceivable to implement the getAs method by the abstract class AMeasure while delegating to the derived classes the only responsibility of voting for such a conversion.

Pages: 1, 2

Next Pagearrow