A Java library for typed quantities and units of measurement, implementing JSR-385 (javax.measure, unit-api 2.2). It is in development.
Status: milestone 6 done (units core, typed double and BigDecimal quantities for all 33 quantity types, eight catalogs with metric and binary prefixes, quantity.toSystem(system), unit and quantity formats with locale-aware numbers, a decimal-preserving parse and a faithful form for offset units, Java serialization of units and quantities, user .units files loaded at run time, the JSR-385 service provider). The systems are SI (128 distinct units), USCustomary (35), Imperial (42), CGS (16), ESU (22), EMU (29), Gaussian (26, listed as 27 symbols in docs/UNITS.md because Fr and statC are the same unit) and Information (22: bit and byte with decimal and binary prefixes); quantity.toSystem(system) converts to the system's preferred unit for the quantity type. The JSR-385 TCK (unit-tck 2.2) passes: 113 of 113 tests; mvn -q -B clean verify runs 1270 tests (core 567, codegen 179, uom 409, tck 113, examples 2). Milestone 7 prepared the release (POM metadata, an inactive release profile, the public API pin, the generated unit table, the documentation set, scripts/verify-release.sh and a CI workflow on JDK 17, 21 and 25); nothing has been published. Not released.
Length marathon = Length.of(42.195, SI.KILOMETRE);
System.out.println(marathon.to(USCustomary.MILE)); // prints: 26.218757456454306 mi
Speed pace = marathon.divide(Time.of(3, SI.HOUR)).as(Speed.class);
System.out.println(pace.to(USCustomary.MILE_PER_HOUR)); // prints: 8.739585818818103 mph
Temperature boiling = Temperature.of(100, SI.DEGREE_CELSIUS);
System.out.println(boiling.toSystem(USCustomary.SYSTEM)); // prints: 212.0 °F
Length exact = Length.of(new BigDecimal("0.1"), SI.METRE).add(Length.of(new BigDecimal("0.2"), SI.METRE));
System.out.println(exact); // prints: 0.3 m
UomQuantityFormat format = new UomQuantityFormat(UomUnitFormat.standard());
System.out.println(format.parse("20 km").as(Length.class).to(SI.METRE));
// prints: 20000.0 mQuantities are typed (Length, Speed, ... for all 33 JSR-385 quantity types), in double or exact BigDecimal
precision; the catalogs SI, USCustomary, Imperial, CGS, ESU, EMU, Gaussian and Information hold the units
(docs/UNITS.md lists them all), and quantity.toSystem(system) converts to the preferred unit of a system.
A system is a catalog of units plus a preferred unit for each of the 33 quantity types. Every unit is an exact factor
of SI units over the SI dimensions: CGS holds cm, g and s, but they are not base units of their own, so
getSystemUnit() is always the SI unit, and a quantity without a type, such as a product in kg·m/s, has no unit in a
system (toSystem throws UnsupportedOperationException; convert it with to(unit)). For ESU, EMU and Gaussian
this means that values convert correctly, but the formulas of those systems do not hold: in Gaussian units a charge has
the dimension g^½·cm^3/2·s^-1 and the B and H fields share a dimension, while here every unit keeps its SI dimension,
so the gauss and the oersted are not compatible. Systems with base units or dimensions of their own are out of scope
(docs/DESIGN.md, DR-13).
System.out.println(Force.of(1, SI.NEWTON).toSystem(CGS.SYSTEM)); // prints: 100000.0 dyn
System.out.println(ElectricCharge.of(1, SI.COULOMB).toSystem(Gaussian.SYSTEM)); // prints: 2.99792458E9 Fr
System.out.println(EMU.GAUSS.isCompatible(EMU.OERSTED)); // prints: falseJava 17 or later. Not published yet; after mvn -q -B install -DskipTests in this repository:
<dependency>
<groupId>org.tinygears</groupId>
<artifactId>uom</artifactId>
<version>0.1.0-SNAPSHOT</version>
</dependency>uom depends on uom-core and the JSR-385 API; the only third-party runtime dependency is
javax.measure:unit-api 2.2. On the module path, requires org.tinygears.uom;
(module org.tinygears.uom requires org.tinygears.uom.core and java.measure transitively).
- docs/CUSTOM-UNITS.md: define your own units in
.unitsfiles and load them at run time. - docs/PROVIDERS.md: uom next to Indriya or another JSR-385 implementation: which provider is current and why uom works with either.
- docs/INDRIYA.md: where uom and Indriya, the reference implementation, behave differently
(equality,
isEquivalentTo, numbers, formats and parsing). - docs/OFFSET-UNITS.md: units with an offset (°C, °F): points and differences,
Quantity.Scale, and what conversion, sums, scaling and comparison do for each. - docs/UNITS.md: every unit of the built-in catalogs (generated).
- docs/API.md: what the public API is, semantic versioning, deprecation.
- SECURITY.md: serialization filter, reporting a vulnerability.
- CHANGELOG.md: changes per release.
- CONTRIBUTING.md: build, regeneration, tests, conventions.
- AGENTS.md: the rules for AI coding agents (
CLAUDE.mdlinks to it). - docs/RELEASING.md: the release procedure (for the maintainer).
- docs/REQUIREMENTS.md, docs/DESIGN.md and docs/TODO.md: requirements, design and open decisions.
- docs/NATIVE-API.md: proposal for a native API next to the JSR-385 implementation (not decided).
- LICENSE: Apache License 2.0.
uom-core: units, dimensions, converters, the.unitsparser, the shareddoubleandBigDecimalquantity base classes.uom: the library to depend on (JPMS moduleorg.tinygears.uom, requiresuom-coretransitively). Packageorg.tinygears.uom.quantity: generated typed quantity interfaces,doubleandBigDecimalimplementations and factories (33 types, 133 generated files). Packageorg.tinygears.uom.systems: the generated catalogs (SI,USCustomary,Imperial,CGS,ESU,EMU,Gaussian),UomSystemOfUnits, unit and quantity formats, theServiceProvider.uom-codegen: the generator (internal, not published).uom-tck: runs the JSR-385 TCK againstuom(test only, not published).uom-examples: runnable usage examples (not published); see below.
Build and test everything: mvn -q -B clean verify (see CONTRIBUTING.md).
Examples.java shows typed quantities and conversion, dividing quantities and narrowing with as(Speed.class) or Energy.from, offset units, equals versus isEquivalentTo, isCloseTo with a relative or absolute tolerance (closeness()), unit and quantity parsing and formatting, and use through the JSR-385 ServiceProvider. decimals() shows BigDecimal quantities: 0.1 m + 0.2 m is exactly 0.3 m, unlike with double. mixedPrecision(), contexts(), decimalEquality() and decimalFormat() show the precision rule (the decimal wins: decimal 0.1 m plus double 0.2 m is exactly 0.3 m), MathContext and toDecimal/toDouble, equals versus isEquivalentTo across precisions, and decimal formatting. systems() shows quantity.toSystem(system): 1 ft in CGS is 30.48 cm, 100 °C in US customary is 212.0 °F (exactly 212 °F for a BigDecimal), the US gallon (3.785411784 L) against the imperial gallon (4.54609 L), and 1 C in Gaussian units. information() divides 1.5 GiB by 100 Mbit/s (128.84901888 s), converts it to 1610.612736 MB and formats 1024 B as KiB but 1000 B as kB. serialization() round-trips a unit and a quantity through a byte array, locales() prints and parses 1,5 km in German, offsetUnits() formats an unnamed shifted kelvin as K + 5 and parses it back, decimalParse() parses 1.50 km to a double by default and, with withDecimals, 1.234567890 km to a BigDecimal quantity that keeps its scale and narrows with Length.from (the context rounds only later arithmetic, and a unit without a quantity type gives an untyped decimal quantity), and userUnits() loads a small .units text, parses 2 league and converts it with toSystem(system). ExamplesTest checks the printed output. Run it after a build with:
mvn -q -B -pl uom-examples -am install -DskipTests
mvn -q -B -pl uom-examples exec:java -Dexec.mainClass=org.tinygears.uom.examples.Examples
The typed interfaces have the simple names of the standard ones (org.tinygears.uom.quantity.Length extends javax.measure.quantity.Length), so a file that imports ours cannot also import the standard type. Declare units as Unit<?> or with var (or name the standard type fully qualified), and quantities with our types:
var km = SI.KILOMETRE; // UomUnit<javax.measure.quantity.Length>
Length run = Length.of(10, km);Every quantity of this library is a UomQuantity: the JSR-385 Quantity with our typed interface as the return type
of its operations, plus what the standard has no method for. Products, quotients and UomQuantityFormat.parse return a
UomQuantity, so:
as(Speed.class)narrows to our typed interface, by dimension asasTypedoes:length.divide(time).as(Speed.class).asTypecannot take our interface (Speedis not aQuantity<Speed>), so it needs the standard class and a cast.isCloseTo(that, 1e-9)(relative) andisCloseTo(that, Length.of(1, SI.MILLIMETRE))(absolute) compare within a tolerance you choose. The standardisEquivalentTois exact for decimal quantities and uses a fixed1e-12fordoublequantities.toSystem(system),toDouble()andtoDecimal(mc)convert to a system's preferred unit and between number types.
Angle degrees = Angle.of(BigDecimal.ONE, SI.RADIAN).to(SI.DEGREE); // rounded to 34 digits
System.out.println(degrees.isEquivalentTo(Angle.of(BigDecimal.ONE, SI.RADIAN))); // prints: false
System.out.println(degrees.isCloseTo(Angle.of(BigDecimal.ONE, SI.RADIAN), 1e-30)); // prints: trueA quantity whose static type is the standard Quantity<?>, such as one from code written against JSR-385 only, is
narrowed with X.from: Speed.from(quantity) narrows the same way, returns a quantity of this implementation as it is,
and refuses one of another implementation with a ClassCastException.
UomUnitFormat.standard() parses and formats unit symbols and formulas (kg·m/s^2) over all eight systems. new UomQuantityFormat(unitFormat) formats a quantity as value, space, unit, in Locale.ROOT; it is immutable:
withLocale(Locale.GERMANY)writes and reads the number in that locale (1,5 km); unit symbols are never localized. Grouping separators are never written and are a parse error unlesswithGrouping(true).withDecimals(MathContext.DECIMAL64)parses toBigDecimalquantities that keep every digit and the scale of the text (1.50 kmhas scale 2); the default parses todouble.- A
jlinkimage of an application that formats in a locale other than English needs the modulejdk.localedata(--add-modules jdk.localedata); without it,Locale.GERMANYformats1.5 km. - An offset unit without a symbol formats as a shifted formula (
SI.KELVIN.shift(5)isK + 5,5|9·K + 459.67is the degree Fahrenheit) and parses back to an equal unit. Units with a negative or root factor, an irrational shift or a formula over 10,000 characters have no faithful form:formatthrowsIllegalArgumentException. A glued minus before an integer (s-1) is an error: writes^-1ors - 1.
UomQuantityFormat german = new UomQuantityFormat(UomUnitFormat.standard()).withLocale(Locale.GERMANY);
System.out.println(german.format(Length.of(1.5, SI.KILOMETRE))); // prints: 1,5 km
UomQuantityFormat decimals = new UomQuantityFormat(UomUnitFormat.standard()).withDecimals(MathContext.DECIMAL64);
Quantity<?> parsed = decimals.parse("1.50 km");
System.out.println(parsed + " " + ((BigDecimal) parsed.getValue()).scale()); // prints: 1.50 km 2
System.out.println(UomUnitFormat.standard().format(SI.KELVIN.shift(5))); // prints: K + 5Units and quantities are Serializable through serialization proxies that re-check every invariant on reading. Never read untrusted streams without an ObjectInputFilter: SECURITY.md has the recommended filter and the details. The serialized form may change before 1.0. Systems and formats are not serializable.
A .units file defines units, one per line as symbol:name formula (_ in a name is a space); UomSystemOfUnits.load reads it at run time. The units load as Unit<?>; narrow them to a built-in quantity type with asType. docs/CUSTOM-UNITS.md is the full guide.
@system Medieval # system name (else the file name)
league:league 3 mi # formulas use the built-in symbols
furlong:furlong 220 yd
rod:rod 1|40 furlong # exact fraction
@prefixed league k # adds kleague
@preferred Length furlong # q.toSystem(system) gives furlongs
UomSystemOfUnits medieval = UomSystemOfUnits.load(Path.of("medieval.units"));
UomQuantityFormat format = new UomQuantityFormat(UomUnitFormat.standard().with(medieval));
Length d = format.parse("2 league").as(Length.class);
d.toSystem(medieval); // 48.0 furlongThe quantity sources in uom/src/main/java/org/tinygears/uom/quantity, the catalogs in uom/src/main/java/org/tinygears/uom/systems/ and docs/UNITS.md are generated by uom-codegen from data/ and committed; CONTRIBUTING.md has the regeneration command.