Exploring Java's Units of Measurement API (JSR 385)
Dealing with unit conversion is always a pain point. At first, it seems simple enough until you hit the first edge-case. Just like with Date and Time (JSR 310), there’s a well-specified solution available, although not directly in the JDK: the Units of Measurement API (JSR 385).
Table of Contents
There’s going to be a mix of British English (BE) and American English (AE) writing of unit names throughout the article, as the library I’m talking about uses BE for constants, etc. However, I’m used to writing AE, and most of my audience is most likely too.
Dealing with Units is Hard
Working with different units of measurement in our code can be challenging. While we have well-specified and standardized SI units, their consistent and correct use or conversion remains complex.
And the problem is getting worse when non-SI units, such as United States customary units or the Imperial units, are thrown into the mix.
12 inches is a foot
3 feet is a yard
22 yards is a chain
10 chains is a furlong
8 furlongs is a mile
3 miles is a league
…
This often leads to potential conversion errors, inaccuracies, and inefficiencies.
There are many real-life examples of things going totally wrong in places you’d expect them to know better.
Notable Unit Conversion Errors
In 1983, Air Canada Flight 143 ran out of fuel mid-flight due to a series of issues. A faulty “fuel-quantity indicator sensor” was replaced with another nonfunctional one. To refuel the plane, a dripstick was used to manually measure the volume and calculate the required amount. The Boeing 767 was the first of its type in the Air Canada fleet, and it used kg for fuel, unlike all the other planes and all the manuals, which used lb. This resulted in the plane only carrying ~45% of the required fuel load for its trip.
In 1999, NASA lost the $125 million Mars Climate Orbiter due to miscalculations by parts of the ground software provided by Lockheed Martin using United States customary units instead of the required SI units.
In 1999, Korean Air Cargo Flight 6316, originating in Shanghai, got clearance to climb to 1.500m, which it did. The first officer mistook the “1.500” as feet and concluded they should descend to 1.500 feet (~460m). They descended too fast and lost control, eventually crashing the plane. Almost all countries use feet to measure altitudes in compliance with the International Civil Aviation Organization convention, except China, Russia (until 2017), and North Korea.
In 2003, a roller coaster train derailed at Space Mountain ride in Tokyo Disneyland due to faulty axles. The original specifications were converted from Imperial unit to the metric system, which introduced an error that wasn’t discovered when replacement parts were ordered.
As you can see, there are a lot of ways dealing with units can go wrong, especially in high-stress situations, like taking off from a busy airport or needing to refuel a plane quickly. In the best-case scenario, you can glide safely to an airport, as Air Canada Flight 143 did. Worst-case scenario, however, the plane crashes, as Korean Air Cargo Flight 6316, killing eight people and injuring 42.
That’s why we absolutely need to know about the problems and caveats of working with different units in our code.
What’s the Unit of Measurement API
JSR 385, also known as the Units of Measurement API 2.0, is a Java Specification Request aiming to provide a standardized and unified API for handling units and quantities.
History of the API
Like Java Enhancement Proposals (JEP), JSRs are often superseded by newer requests, and the origins of JSR 385 reach back to multiple earlier ones, like JSR 108 (2001-2004, withdrawn), JSR 275 (2005-2010, rejected), and JSR 363 (2014-2016, released as 1.0).
In February 2022, version 2.1, and version 2.2 was released in May, 2024.
Specification Vs. Implementation
Unlike another JSR you’re most likely familiar with, JSR 310, the Date and Time API introduced in Java 8, the Units of Measurement API is not included in the JDK.
Instead, it’s a dependency you need to include with the code residing in the javax.measure
package.
Furthermore, the dependency is only the specification, which includes only interfaces, not implementation in the form of classes. The Java/Jakarta EE dependencies work similarly. The downside, however, is the need for a reference implementation to actually use the API. Thankfully, JSR 385 got as covered.
A Taste of Indriya
The people behind the API also provide a reference implementation called “Indriya”.
The project not only implements the API but also provides some additional types, like MixedQuantity
.
The API and reference implementation both support Java 8, with some additions in the case of Java 9+. However, the code examples throughout the rest of the article will use more modern Java features.
Enough about technicalities, it’s time for some code!
How to Use javax.measure
The API is build on three concepts:
- Units
- Quantities
- Dimensions
Each of them has a corresponding type in the API:
- []
javax.measure.Unit<Q extends Quantity<Q>>
](https://unitsofmeasurement.github.io/unit-api/site/apidocs/javax/measure/Unit.html) javax.measure.Quantity<Q extends Quantity<Q>>
javax.measure.Dimension
The interfaces provide methods for unit conversion, arithmetic operations, and more.
The reference implementation provides the additional types with static
factory methods, constants, etc., to create any of the three base types as required.
Units and Quantities
A Unit
is defined by its symbol, name, and dimension.
Take Kelvin (name), for example.
It’s a temperature (dimension) with the symbol “K”.
There are two ways to create a Unit
:
- Using a constant from the
tech.units.indriya.unit.Units
type - Combining a
Unit
with ajavax.measure.Prefix
Using a constant is the simplest way:
Indriya provides a variety of 43 units based on the 33 defined Quantity
implementations of the API:
Acceleration
AmountOfSubstance
Angle
Area
CatalyticActivity
Dimensionless
ElectricCapacitance
ElectricCharge
ElectricConductance
ElectricCurrent
ElectricInductance
ElectricPotential
ElectricResistance
Energy
Force
Frequency
Illuminance
Length
LuminousFlux
LuminousIntensity
MagneticFlux
MagneticFluxDensity
Mass
Power
Pressure
RadiationDoseAbsorbed
RadiationDoseEffective
Radioactivity
SolidAngle
Speed
Temperature
Time
Volume
Even though the available constants already cover a lot of use-cases, their real power lies in combining units with a javax.measure.Prefix
.
A Prefix
prepends a unit of measurement to indicate multiples or fractions of that unit.
For example, there’s no constant for kilometre.
Instead, we’re supposed to use MetricPrefix.KILO
in combination with Units.METRE
:
The API gives us two Prefix
implementations:
MetricPrefix
(24 variants)BinaryPrefix
(8 variants)
With a Unit
, we can create a corresponding Quantity
.
The simplest way to create a Quantity
instance is using one of the static
factory methods of tech.units.indriya.quantity.Quantities
called getQuantity(...)
:
Mixing Quantities
Some values are easier to represent by multiple units, such as “1m, 74cm”. We could just say “174 cm”, but in many scenarios, splitting up the values can make more sense.
Indriya got you covered by giving us two approaches to create “mixed-radix” quantities.
The first one is MixedQuantity<Q extends Quantity<Q>>
, which doesn’t actually implement Quantity<Q extends Quantity<Q>>
but QuantityConverter<Q>
instead.
Its only purpose of MixedQuantity
is to provide an easy way to create a type holding multiple Quantity
instances and then converting it to another unit, making it a real Quantity
:
The second option is using one of the Quantities
factory methods.
The code is not as easy on the eyes as MixedQuantitiy
, but we neither need Quantity
instances nor do we need the conversion step:
Unit Dimensions
As mentioned before, part of a Unit
definition is a Dimension
.
This type represents the physical dimensions of the unit, used to categorize non-interchangeable quantities.
Physical quantities can be a single base dimension, such as length, mass, time, etc., or a combination, like kilometers per hour.
The Dimension
type provides a standardized representation to ensure that different units of the same physical quantity are correctly compared and converted.
The type tech.units.indriya.unit.UnitDimension
provides 8 dimensions:
NONE
LENGTH
(L)MASS
(M)TIME
(T)ELECTRIC_CURRENT
(I)TEMPERATURE
(Θ)AMOUNT_OF_SUBSTANCE
(N)LUMINOUS_INTENSITY
(J)
The Dimension
type is crucial for accurate unit handling by ensuring that operations with units and quantities adhere to physical laws.
Operations involving dimensions must respect their distinct properties, such as not confusing mass with length or volume with time. When physical measures are multiplied or divided, the resulting dimensions reflect these operations, like “length squared for area” when multiplying two meter-based quantities.
Creating Custom Units
A lot of bases are already covered by the available units, but what if you need the “banana equivalent dose”?
The banana equivalent dose (BED) is an informal measure of radiation exposure. We absorb ~0.1 µSv when consuming an average banana.
If we look at the implementations of tech.units.indriya.AbstractUnit<Q extends Quantity<Q>>
in the tech.units.indriya.unit
package, we see the different options for creating a Unit
instance.
AlternateUnit<Q extends Quantity<Q>>
An alternative unit format, like Ohm is Volt divided by Ampere.
This works for system units, not for calculation-based units.
AnnotatedUnit<Q extends Quantity<Q>>
Units that are based on another one, but add an annotation:
The annotation only affects formatting.
BaseUnit<Q extends Quantity<Q>>
You might have guessed it, but this is the lowest level unit that other units build up on:
There’s no static of(...)
factory method available for some reason.
ProductUnit<Q extends Quantity<Q>>
Units that are products of other units, like square and cubic meters:
There are static of...
factory methods available, but they require casting to make the compiler happy:
TransformedUnit<Q extends Quantity<Q>>
A TransormedUnit
is derived from other units, using a UnitConverter
to calculate the new value.
Perfect for our “banana problem”!
Even after devouring 5,000 bananas, we’re far away from the lethal dose of 3.5 Sv.
The UnitConverter
interface is used to convert between different units and the package tech.units.indriya.function
provides many different operations.
Let’s take a look at a unit that might be more common than BED…
We can concatenate multiple converters to create more complex units, like Fahrenheit:
It’s still easy on the eyes and straightforward.
Most custom units will most likely by single multiplications with a base unit to start from. Take a look at the OpenHAB project, an open-source home automation software which uses Indriya and implements Imperial units in a few lines of code.
Safer Calculation with Multiple Units
To illustrate how to calculate with different units, let’s find out how many American football fields (100 yards excluding endzones) are needed to reach the moon.
First, we need to declare the missing units, and not just yard in relationship to metre:
We can even go a step further and make “football field” an custom unit!
This way, the calculation becomes even simpler/more readable. Even though I’d recommend sticking to official units.
And the result is *drum roll*
Well, that’s not really helpful…
The JavaDoc of divide
explains what happened:
Returns the quotient of this
{@code Quantity}
divided by the{@code Quantity}
specified.
To convert the quotient to an actual value, wee need to call toSystemUnit()
:
And there you have it! It’s approx. 4.203.850 American football fields to the moon, excluding endzones.
Formatting Units and Quantities
Formatting is done via an implementation of UnitFormat
and QuantityFormat
.
UnitFormat
There are three implementations available:
EBNFUnitFormat
LocalUnitFormat
SimpleUnitFormat
Usually, the singleton instance is retrieved via static getInstance()
and one of the format(...)
methods is used.
However, there are static
factory methods available to create specialized/customized variants.
The provided formatters support to specify the label for a unit:
Because Unit
implements boolean equals(Object obj)
, the label override is used for any equal unit:
LocalUnitFormat
is a locale-sensitive formatter and provides partial translations for the following Locales:
- Arabic (ar)
- Chinese (cn)
- German (de)
- English/Indonesia (en_ID)
- English/India (en_IN)
- English/Malaysia (en_MY)
- English (en)
- Japanese (ja)
- Norway (no)
- Portugese (pt)
- Russian (ru)
- Swedish (sv)
- Thai (th)
EBNFUnitFormat
is a locale-neutral format that uses an Extended Backus-Naur Form grammar for parsing.
Check out the class
document for details.
QuantityFormat
The QuantityFormat
actually extends java.text.Format
.
Two implementations are provided:
NumberDelimiterQuantityFormat
SimpleQuantityFormat
The first one, NumberDelimiterQuantityFormat
, combines NumberFormat
and UnitFormat
with a specific delimiter.
To create your own, use the tech.units.indriya.format.NumberDelimiterQuantityFormat.Builder
:
The SimpleQuantityFormat
is pattern-based.
To remove the usual space between the number (n
) and the unit (u
), try this:
There are a few restrictions, like the unit has to come after the value, and nothing is allowed after the unit. Check out the type’s JavaDoc for more information about the possible patterns.
Conclusion
The Unit of Measurements API is quite fascinating to me.
On the one hand, it’s a perfect way to mitigate the common problems when working with measurements:
- Conversion errors
- Inconsistency
- Precision/rounding issues
- Ambiguity and miscommunication
- Handling compound units
The reference implementation offers a robust framework for creating safer and more expressive code dealing with SI units, and adding other units is straightforward, too.
On the other hand, though, it’s another layer that can make things more complicated than it needs to be.
If you don’t need to deal with different units and conversions, there’s no need to use specific types to represent values except for increased expressiveness.
And what about persistence or interoperability with other code that doesn’t know about Quantity
et.al.?
In my opinion, that’s the reason why JSR 385 wasn’t included directly in the JDK, whereas JSR 310, the Date and Time API, was.
It’s a powerful API, but niche, for specific use cases that require such a way of doing things.
Still, it’s not a must-have for all your calculation needs. However, knowing about a reliable and mature API for straightforward unit handling when needed is advantageous, so I wrote this article about it.
Resources
- JSR 385 (JCP)
- Units of Measurement (project page)
- Unity of Measurement (API docs)
- A Taste of Indriya (reference implementation)