When I first wrote this note, perhaps 4 or 5 years ago, there didn't
seem to be a lot of interest in standardizing physical units, except
among the scientific community. One example is the
package called "Java Addition to
Default
Environment" (described in
JScience - Java Tools and Libraries for the Advancement of Sciences),
developed
by
J-M.
Dautelle.
This
is
mostly
run-time,
but
it
now
has
a
package of "measurements" that is
strongly typed, and in fact there seems to be a growing awareness that
a run-time
facility, although it can be very comprehensive, does not protect you
against some potentially catastrophic errors.
Your system may run
beautifully
for years because it seldom executes a particular path in the code, and
then crash unexpectedly in the middle of the night!
Although
this provides a lot of flexibility, from a practical point of view this
does not seem adequate. Compile-time checking, on the other hand,
runs into various language limitations, and it is hard to form a
consensus that accommodates all the various requirements. Over
the last few years, a large number of
different packages have been developed, typically within individual
industries, in response to their particular needs, but it appears that
(speaking as of today -
August 2010!) we have not made great progress in agreeing on an
industry-wide
standard. A colleague, Denis Garneau, recently retired from IBM,
and I have been trying to come up
with a type-safe implementation of physical units in Java for some time
now, and
we have come up with some ideas, based partly on
our
experience with business data types, which I am describing below.
This preliminary work came to the attention of Werner Keil of Creative
Arts and Technologies, who is active in the Java standards
activity, but so far it is not clear how our work should be integrated
with the packages that Werner and his colleagues are developing.
There is no doubt that the run-time approach
avoids
a number of problems, some of which will be described in what
follows.
The JScience project provided a large number of units and sophisticated
dimensional
analysis - mostly at run-time. The units supported by this package were
grouped into two
classes: SI
and Non-SI. SI also contains symbols for the 20 SI prefixes
(positive and negative powers of 10). There are apparently other
approaches to units being worked on in
various
places - NASA says they now have their own, which they may share with
the Java community at some time in the future - but standardization of
the units of measure is still very much a work in
progress. NASA was probably strongly motivated by the loss of the
Mars Lander, alluded to in my web page entitled "Smart Data".
A related problem with either type of facility is that it cannot
support all the units in use worldwide, so we need a way for an
application
to add its own unit definitions. It seems to make sense to partition
symbols
by dimension - at least the non-SI symbols.
It is reasonable to assume that the SI symbols are pretty much
standarized (except for the spelling issue for meters/metres,
liters/litres, etc.), but
the non-SI measures make for a pretty unwieldy collection. As J-M.
Dautelle points out,
the American and British measures are often different, e.g. ton,
gallon,
cup, and the two sides of the Atlantic are supposed to be using the
same language! So, it seems
as though we should group the non-SI units by dimension and language. However, my
hope is
that we can gradually extend the concepts described below - one
dimension at a time! - without running into an unmanageable
combinatorial complexity! We'll see!
DimensionQuantity is an abstract class with the following
instance
variables:
|
We will hold a physical dimension in two forms (for performance
reasons):
a "reference" unit, into which all values of that dimension will be
converted by their constructors, and an amount in the unit in which the
data
was originally created (on the assumption that the user will probably
want
to see it in that form at some point). E.g. if the user
entered
a distance as "6 inches", s/he may want to see it in that form again,
not
15.24 cm. A facility will also be provided to convert quantities to
other units of the same dimension.
AbstractUnit is an abstract class with three instance
variables:
String name; // e.g. "Angstrom" |
In our example, DistanceQuantity
extends DimensionQuantity which provides a number
of general methods, used to build dimension-specific methods.
DistanceUnit contains a number of useful distance
units, defined as public static, plus constructors for
building new ones. Each such definition gives a public name for
the unit, and
specifies how to convert it to the reference unit. Here is a
possible
set of distance units:
|
The DistanceUnit class has (at least) two constructors:
| Constructors |
|---|
| public DistanceUnit (String,
double) public DistanceUnit constructor specifying display String and multiple of reference unit |
| public DistanceUnit (String,
DistanceUnit, double) public DistanceUnit constructor specifying display String, DistanceUnit and multiple of that unit |
Here you will see the standardization problem. My colleague points out that the SI units have standard abbreviations, and these could certainly be used as variable names, except for the cases where the units are derived. However, many measures in common use are not only not standard, but also language-dependent. For instance, we cannot make "foot" a world-wide standard as the corresponding French term is "pied". Some symbols could be written using Unicode characters, as these are acceptable within Java identifiers, but this is somewhat unwieldy if these terms are going to be in common use. For instance, angstrom, shown in the example, is more correctly \u00C5ngstr\u00F6m, which represents "Ångström". I suggest that a practical compromise is to use the DistanceUnit class, to define "locally defined" units for an application - some of these might eventually themselves become de facto standards...
Note that the above definitions provide both a Java variable name,
and
a String for input/output, which would presumably support a wider
choice
of characters. The latter, however, introduces the question of
"localization" - since I suspect there are more languages than unit
names, we should instead have a HashMap for each language, where we can
look up the string representing the unit in a particular language.
There are also the intertwined questions of grammar and syntax: in
English, it is customary to use the singular for one unit, and plural (generally
ending in "s") for zero or more than one. Where
should this information be held - along with any exceptions, such as
"foot" ("feet"), which naturally does not take the "s"?
Here is a preliminary list of possible constructors and methods for the Distance class - as in all OO applications, I'm sure real-life applications will see the need for additional ones. Among the constructors you will notice one that has novalues defined - this is defined as non-public, so it cannot be used outside the class.
Constructors for DistanceQuantity:
| Constructors |
|---|
| public DistanceQuantity
(double, DistanceUnit) public Distance constructor with value initialized from double and DistanceUnit |
| DistanceQuantity () non-public Distance constructor with no values set |
Initial (and probably partial) set of methods (all public):
| returns | Methods |
|---|---|
| DistanceQuantity | add
(DistanceQuantity) add one distance to another, giving result in reference units |
| DistanceQuantity | subtract
(DistanceQuantity) subtract one distance from another, giving result in reference units |
| boolean | eq
(DistanceQuantity) return true if one distance is equal to another |
| boolean | ne
(DistanceQuantity return true if one distance is not equal to another |
| boolean | gt
(DistanceQuantity) return true if one distance is greater than another |
| boolean | ge
(DistanceQuantity) return true if one distance is greater than or equal to another |
| boolean | lt
(DistanceQuantity) return true if one distance is less than another |
| boolean | le
(DistanceQuantity) return true if one distance is less than or equal to another |
| DistanceQuantity | multiply
(double) multiply a distance by a scalar, giving a distance |
| DistanceQuantity | divide
(double) divide a distance by a scalar, giving a distance |
| AreaQuantity | multiply
(DistanceQuantity) multiply one distance to another, giving an area |
| VolumeQuantity | multiply
(AreaQuantity) multiply a distance by an area, giving a volume |
| SpeedQuantity | divide
(TimeInterval) divide a distance by a time interval, giving a speed |
| TimeInterval | divide
(SpeedQuantity) divide a distance by a speed, giving a time interval |
| DistanceQuantity |
convert
(DistanceUnit) convert distance to another unit |
| String |
showInUnits
(DistanceUnit, int) show distance in specified unit, and specified number of decimal places (rounded if necessary) |
This may not be a complete set of methods, but it should be enough to give you a flavour of what I'm driving at. Notice that these methods seem to fall into groups:
Here is an example of how such units might be referenced. And my distances are totally arbitrary! Of course, in real life, the "trip leg" information would be read from a database.
|
Notice that we have used the DistanceUnit class to define
a
"local usage" unit called a "klik", defined as 1.0 kilometres.
An earlier attempt at getting the units right when, say, multiplying
distance by distance to get area used the reference units to effect the
right relationship (each dimension has its own reference unit).
But then I realized that this is somewhat dangerous, as the
combinatorics may get out of hand, so in the implementation of such
mixed dimension operations, I convert
the values to a relevant unit first. E.g. if distances are
converted to centimetres before being multiplied, you can be sure that the resulting area will
be in square centimetres. The result can then be converted to the
desired unit for further processing.
| returns | Methods |
|---|---|
| WeightQuantity | multiply
(AccelerationQuantity) multiply a mass by an acceleration, giving a weight |
However, this results in a logical inconsistency. Since in
this case we would want 1 kilo mass to convert to 1 kilo
weight, we would have to force the acceleration to be 1 - i.e. 1
gravity. In
fact, we all know that the relationship depends on what planet we are
on,
or, more generally, what gravitational field we are in. So let us also
define a class called GFactor, with values such as "nullG" (0.0),
"earthG"
(1.0), "moonG" (0.17), etc. Now we can define a more pragmatic
relationship
between mass and weight, defining a method in the Mass class, as
follows:
| returns | Methods |
|---|---|
| WeightQuantity | calcWeight (GFactor)
determine the weight for this mass, given a G factor |
and its converse (in the WeightQuantity class):
| returns |
Methods |
|---|---|
| MassQuantity | calcMass (GFactor) determine the mass for this weight given a G factor |
In fact, we can keep Acceleration, with the same dimensions as GFactor, but with a different reference unit - Acceleration would use 1 metre/sec2, while GFactor would use 1 "G", which is defined to be 9.80665 metres/sec2.