ONDotNet.com    
 Published on ONDotNet.com (http://www.ondotnet.com/)
 See this if you're having trouble printing code examples


.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.

Bringing It All Together: the Utility Class

We now can write a utility class that programmers can use on the server side when displaying data on a page or saving data from a page. These utility classes play a role to tailor the OO classes available to suit the immediate problem at hand. The primary need here is, as stated, to read values from screens and save them in the database, and vice versa. Please note that the sample code shown for this example is pseudo code.


public class UnitConverter
{
  // Support for reading from screens
  public static IMeasure createImperialWeight(double value, 
                            WeightUnit intendedImperialUnit)
  {
    // region -> imperial:
    return new Weight(value,intendedImperialUnit);

    // region -> metric
    Weight  metricWeight = new Weight(value,
                  intendedImperialUnit.getEquivalentUnit());

    return metricWeight.getAs(intendedImperialUnit);
  }

  //.. Other measures

  // Support for writing to screens
  public static Imeasure convertFromImperial(Imeasure measure)
  {
    //region ->imperial
    return measure;
   
    // region -> metric
    return measure.getAs(measure.getUnit().getEquivalentUnit());
  }
} // end of UnitConverter static class

Reading From an Entry Field

Let us begin with the assumption that all units are internally maintained on the application server and in the database as imperial. We are trying to read a weight field from the screen. The user has entered a number 57 as weight. The code for reading that weight on the server side is as follows:


//static function
Weight w = Converter.CreateImperialWeight(57, //value entered 
                                  //expected weight unit. 
                                  //Will allow Kgs or 
                                  //Pounds on the user side      
                                  Units.Pound.self); 
  

The intention of the CreateImperialWeight is to read a weight unit in pounds, irrespective of the locale in which the Web browser is being operated. If you were to see the Web site in a metric locale, it would have read 57 kgs, and 57 Lbs if it were an imperial region. The function will internally take this into account and always return an object of type Weight, the unit of which would point to an object of type Pound. All quantity conversions will take place internally, returning proper pound quantity.

Let us examine w, the Weight variable:


// Created weight will be in pounds
assert(w.getUnit().name() == Units.Pound.self.getName());

Meaning, if we examine the Unit object that belongs to the Weight measure, we will learn that it will be of type Pounds. To know the double value in pounds, do the following:


// Get the value to store in db
double wValueInPounds = w.getValue();

Let us see how we can get the value w in tons:


//Get value in tons
double wValueInTons = w.getValueAs(Units.Ton.self);

To get the value in tons, simply use the getValueAs function on the Weight measure object by specifying the target unit as Ton. getValueAs takes a unit and returns the converted double value in that target unit.

If we want the target measure as an object instead, we can use getAs on the measure:


Weight wValueInTons = w.getAs(Units.Ton.self);

Writing to an Entry Field

Let us shift focus to printing a field on the screen. Assume we read this value from a database.


// Create a Length measure using imperial
Length lengthFromDB = new Length(100,   // value read from db
                     Units.Miles.self); // value in miles

// Obtain a length measure in the target Unit irrespective of the region
Length lengthOnTheScreen = 
               UnitConverter.convertFromImperial(lengthFromDB );

convertFromImperial will take an imperial unit and return a region-dependent measure. In the above example, if the region is imperial, then the returned unit is the same. If it is metric, then it will be in kilometers. How does this function know it is kilometers, when all we said was Miles? A Unit class definition (as it was presented above) includes an equivalent unit setting for each of the units in the other system. So, a mile will point to a kilometer and a kilometer will point to a mile as comparable measures. Here is a code segment that gives you a variety of values that you can put on the screen:


// will be in miles or kilometers depending on the user locale
double lengthOnTheScreenValue = lengthOnTheScreen.getValue(); 
  
// Ex: Km.
string lenghtOnTheScreenShortLabel = 
                  lengthOnTheScreen.getUnit().getShortLabel(); 

// Ex: Kilometers           
string lenghtOnTheScreenLongLabel = 
                  lengthOnTheScreen.getUnit().getLongLabel(); 

Converting Between Units

Also, we can convert units from one to another.


Length    len1InCm = new Length(100,Units.Cm.self);    
Length    len1InMeters = len1InCm.getAs(Units.Meters.self);
assert(len1InCm == len1InMeters)

Conclusion

The solution to the problem demonstrates how well OO principles come to the rescue of internationalization. This is because once abstracted, objects have the ability to suit the locale due to their polymorphic nature. This is OO 101. You can even extend these measures by defining manipulation operators such as addition, subtraction, etc. We see the following advantages in the suggested approach:

  1. Unit conversions are type safe.
  2. IDE will prompt you for the defined units so far in the system. This finite set of units is a good compromise between complete openness and ease of use.
  3. It is trivial to convert from one unit to another.
  4. This suits the internationalization issue quite well.

Nevertheless, there are areas that are not explored in this short article, such as precision, for instance. Not much thought was given to a page that contains mixed mode units: metric and imperial. Also, getting the labels from resource files will be necessary for localizing labels.

Additional Localization References

Copyright © 2009 O'Reilly Media, Inc.