Units Checker for JSR 308

This is a simple package for units and dimensions, based on Andrew Kennedy's Ph.D. work. It includes dynamic checks for unit compatibility. In addition, it can statically check for errors using an annotation processor with an experimental version of the JSR 308 tools. Specifically, the experimental version supports annotation arguments that are Java expressions.

The package was developed by Lex Spoon.

Prerequisites

To use this system, check out the JSR 308 langtools and checker framework. Put them in sibling directories, and name the directories checkers and langtools. Then, apply this patch with patch -sp1. (TODO: convert the patch to a Mercurial branch) Build langtools and checkers as usual, and use the resulting javac for the rest of this tutorial.

A quick example

The following import pulls in the units API:

import checkers.units.model.*;

Additionally, many standard units, such as meters and seconds, are available in class StandardUnits. These can be imported as follows:

import static checkers.units.StandardUnits.*;

Given those imports, expressions like the following are legal:

Measure distance = m.times(5);  // 5 meters
Measure time = s.times(10);     // 10 seconds
Measure speed = distance.div(time);   // 0.5 meters per second

Expressions that have mismatched units will raise a run-time exception:

Measure nonsense = distance.plus(time);  // IncompatibleUnitsError

Here is a complete example program putting it all together:

import checkers.units.model.*;
import static checkers.units.model.StandardUnits.*;

public class UnitsExample {
  public static void main(String[] args) {
    Measure distance = m.times(5);
    Measure time = s.times(10);
    Measure speed = distance.div(time);

    System.out.println("Speed = " + speed);

    Measure nonsense = distance.plus(time);
  }
}

Compile and run the example with checkers-quals.jar on your classpath:

../langtools/dist/bin/javac -cp ../checkers/checkers-quals.jar UnitsExample.java
java -cp ../checkers/checkers-quals.jar:. UnitsExample

You should see a printout of the speed followed by an IncompatibleUnitsError:

Speed = 0.5 m/s
Exception in thread "main" checkers.units.model.IncompatibleUnitsError: Incompatible units: s vs. m

The model: units and dimensions

There are three types you need to be familiar with to use this system. They are:
Measure
A scalar plus a unit. For example, 5 kilometers.
Unit
A unit of measure. For example, kilometers.
Dimension
The property that compatible units must share. For example, Length.

The library will automatically propagate units through your computation. It will attempt to maintain the units you enter when possible, but will convert them if you add or subtract units that are compatible but different. Any attempt to add or subtract measures with incompatible units will result in an IncompatibleUnitsError.

Checking units at compile time

Units errors can be found at compile time rather than at run time. To do so, simply compile with checkers.units.UnitsChecker as an annotation processor. For example:

../langtools/dist/bin/javac -cp ../checkers/checkers.jar \
  -processor checkers.units.UnitsChecker \
  UnitsExample.java

In this case, no errors are produced. That's because all of the variables in this example are annotated as an arbitrary Measure. The checker ignores operations on an unannotated Measure, just like a gradual type checker ignores operations on an untyped variable.

To make the checker useful, you must add annotations to the Measure types to indicate the expected dimension of the measure involved. The annotation type is Dim. It is available via the following import: import checkers.units.quals.Dim; The argument to Dim is a dimension, such as Length or Time. Here is an updated example with Dim annotations on all Measure types.

import checkers.units.model.*;
import static checkers.units.model.StandardUnits.*;
import checkers.units.quals.Dim;

public class UnitsExample {
  public static void main(String[] args) {
    @Dim(Length) Measure distance = m.times(5);
    @Dim(Time) Measure time = s.times(10);
    @Dim(Length.div(Time)) Measure speed = distance.div(time);

    System.out.println("Speed = " + speed);

    @Dim(Length) Measure nonsense = distance.plus(time);
  }
}
With the annotations in place, the following error is emitted:
UnitsExample.java:13: (Incompatible types: Length vs. Time)
    @Dim(Length) Measure nonsense = distance.plus(time);
                                                 ^
1 error

As you can see, if you want to get the most of the static units checker, you must systematically use a Dim annotation on every Measure type.

When using a Dim annotation, you must supply an argument that the processor understands, or it will complain. The argument must be a static dimension expression. A static dimension expression is any of: