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:
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 |
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.
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.
|
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
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);
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();
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)
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:
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.
Copyright © 2009 O'Reilly Media, Inc.