Open-source project
iliekturtles/uom avatar
iliekturtles/uom

uom: dimensional analysis as a Rust type system, with no runtime cost

Units of measurement -- type-safe zero-cost dimensional analysis

1,260 stars131 forksRustApache-2.0

At a glance

What is it?
uom is a Rust crate that gives you type-safe, zero-cost dimensional analysis, working with quantities rather than units so that unit conversion happens only at the boundaries of your system. It ships the SI system built in, supports a wide range of underlying storage types, and the current crate version is 0.38.0.
Who is it for?
Adopt uom if you are writing Rust code where mixing up units has real consequences, in engineering, physics or simulation work, and you want the compiler to reject length plus time rather than relying on a review convention. Do not adopt it if the cost of normalising to base units and the compile-time weight of a macro-generated type system outweighs that safety, or if you need exact decimal arithmetic for values that do not fit the storage type you choose.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Activity is slowing. The repository last received commits 6 months ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The design decision that explains everything: quantities, not units

uom is a crate that does automatic type-safe zero-cost dimensional analysis, and the Design section of the README explains the central choice. Rather than working with measurement units, meter, kilometer, foot, mile, uom works with quantities, length, mass, time. This simplifies usage because units are only involved at interface boundaries: the rest of your code only needs to be concerned about the quantities involved.

That is a genuinely different model from most unit libraries, and it is worth being concrete about what changes. A library where you hold a Length value that is implicitly in metres means your arithmetic never has to think about units at all. Adding two lengths is adding two numbers. Multiplying two lengths gives an area. Dividing a length by a time gives a velocity. The type carries the dimension, so the operation is checked and the unit is an implementation detail of the storage.

The README gives the motivating example: no more crashing your climate orbiter, with a link to the Mars Climate Orbiter, which is the canonical case of a unit confusion that destroyed a spacecraft. The crate is Rust, licensed Apache-2.0 OR MIT, with a rust-version of 1.68.0, and the topics are dimensional-analysis, rust and si.

The pre-built system is the International System of Units, which is based on the International System of Quantities. The README links the JCGM documents for SI, ISQ, quantities and measurement units, which is a sign that the author is working from the actual standards rather than from an ad hoc list of unit types. The SI system includes numerous quantities, length, mass, time, and so on, with conversion factors for even more numerous measurement units.

The zero-cost claim is specific and checkable. The README says operations on quantities, plus, minus, times, divide, have zero runtime cost over using the raw storage type, for example f32. That is the property that makes the library adoptable in performance-sensitive code, and it comes from the type system doing the work at compile time rather than from any runtime representation of units.

How the type system is generated, and what a wrong operation looks like

The example in the README shows the mechanism in about fifteen lines, and the shape of it is the point.

rust
extern crate uom;

use uom::si::f32::*;
use uom::si::length::kilometer;
use uom::si::time::second;

fn main() {
    let length = Length::new::<kilometer>(5.0);
    let time = Time::new::<second>(15.0);
    let velocity/*: Velocity*/ = length / time;
    let _acceleration = calc_acceleration(velocity, time);
    //let error = length + time; // error[E0308]: mismatched types

    // Get a quantity value in a specific unit.
    let time_in_nano_seconds = time.get::<uom::si::time::nanosecond>();
}

Three things are happening. Length::new::<kilometer>(5.0) constructs a length from a value and a unit type parameter, and the storage type comes from the import, uom::si::f32, so the quantity is an f32-backed length. Dividing a length by a time produces a velocity, and the comment shows the inferred type. And the commented-out line, length + time, is marked with the exact compiler error it produces, error[E0308]: mismatched types. That is the whole value proposition in one comment: the mistake that would have been caught by a code review is a compile error.

The macro is where the types come from. The README says uom normalizes values to the base unit for the quantity, and that alternative base units can be used by executing the macro defined for the system of quantities, ISQ! for the SI. The uom-macros/ directory at the top level of the repository is the procedural macro crate that generates the quantity and unit types, and Cargo.toml lists it as a workspace member alongside tests/edition_check and tests/feature_check.

The unit lookup at the end of the example is the boundary case that the design anticipates. time.get::<nanosecond>() takes a quantity that is internally stored in seconds and returns the value expressed in nanoseconds. That is the operation you do when talking to an external system, parse a value with a unit, serialise a value with a unit, or print one. Everywhere else, you are working in quantities, and the units never enter the picture.

Storage types are a compile-time choice with real consequences

The feature list is where this crate gets opinionated, and the number of options is a warning as much as a feature.

Underlying storage types are enabled individually: usize, u8, u16, u32, u64, u128, isize, i8, i16, i32, i64, i128 for the built-in integers, bigint and biguint for arbitrary width integers, rational, rational32, rational64 and bigrational for integer ratios, complex32 and complex64 for complex floating point, and f32 and f64. At least one must be enabled, and f32 and f64 are on by default.

The critical consequence is stated in the README's Design section, and it is the thing to understand before picking one. Because values are normalised to the base unit, some values may not be able to be represented, or cannot be precisely represented, for floating point and rational storage types. The example given: if the base unit of length is meter and the underlying storage type is i32, then values like 1 centimeter or 1.1 meter cannot be represented. 1 centimeter normalises to 0.01 meter, which cannot be stored in an i32.

That is not a bug, it is the design working as intended, and the README's phrasing is that uom only allows units to be used safely. But it means the storage type selection is a real engineering decision with a failure mode, and the failure mode is a panic or a compile-time constraint at the point where a value is constructed, not a silent wrong answer.

The integer features all pull in rational-support, and bigint pulls in bigint-support, which is how the crate reconciles the requirement that a unit's conversion factor must be representable in the storage type. Cargo.toml shows num-traits and typenum as plain dependencies, with optional num-rational, num-bigint and num-complex from the num family, plus optional serde_core for the Serde support, gated behind features.

The integer storage types are not the obvious choice for most scientific code, and the fact that they are supported at all is a signal about the audience. A crate that supports i32 lengths, with an exact rational type, and arbitrary precision bigints, is aimed at people who need exact arithmetic, such as unit conversion in simulation or measurement systems where a float's rounding is not acceptable.

Feature flags, no_std, and what the default build includes

The Cargo feature set gives you control over the compile-time cost as well as the functionality, and the defaults are autoconvert, f32, f64, si and std.

The autoconvert feature is the one with the most interesting explanation. It enables automatic conversion between base units in binary operators, and disabling it only allows quantities with the same base units to directly interact. The README gives the reason: the feature exists to account for compiler limitations where zero-cost code is not generated for non-floating point underlying storage types. So autoconvert is a workaround for the compiler, not a preference, and it has to be on for the zero-cost claim to hold with integer and rational storage.

The si feature includes the pre-built SI system and is enabled by default. The std feature compiles with standard library support, and disabling it compiles uom with no_std, which is enabled by default as well. The serde feature enables serialization and deserialization of quantities with the Serde crate, and it is disabled by default.

Cherry-picking is done with the --no-default-features and --features flags, or in Cargo.toml, and the README gives a worked example of a fully specified dependency with the individual storage types listed out. The docs.rs metadata in Cargo.toml shows what the published documentation is built with, and the list is telling: usize, u32, u64, isize, i32, i64, bigint, biguint, rational, rational32, rational64, bigrational and serde. The published docs therefore cover most of the integer and rational types rather than the floating point ones alone, which is what a reader evaluating the crate will actually look at.

The std feature matters more than it sounds. A no_std-capable dimensional analysis crate is usable in an embedded target, which is a niche but real requirement, and the crate's keyword list in Cargo.toml includes no_std alongside units, measurement, SI and dimensional-analysis, and its categories are no-std, science and value-formatting.

For CI, the repository carries a GitHub Actions badge for the full test suite, a Codecov badge, and the workspace members tests/edition_check and tests/feature_check, which are separate crates that exist to verify the edition and the feature combinations compile. The dev dependencies include quickcheck for property testing, approx for approximate comparisons in tests, static_assertions and serde_json, which is the sort of setup you would expect from a library where the invariants are numeric.

Building your own system, and the four example programs

The examples directory is where the library's flexibility is demonstrated, and the four files each answer a different question.

si.rs shows how to use the pre-built SI system, which is the quick introduction for most people. base.rs shows how to create a set of Quantity type aliases for a different set of base units, and the README notes there are implications of choosing different base units. mks.rs shows how to create a custom system of quantities, the metre-kilogram-second system being the obvious default and a case where you would want your own. unit.rs shows how to add new units to existing quantities in the pre-built SI system, which is the extension point most projects will want first.

The base unit choice deserves a sentence of its own, because it is not a cosmetic decision. The unit a quantity normalises to is the unit it is stored in. If length normalises to metres, then a length of 1 centimetre is 0.01 metres, and that is the number your arithmetic sees. Changing the base unit changes every number your program computes and every value it prints. The README's pointer to implications of choosing different base units is doing real work, and the mks example is the case where you would pick a base unit that is not the SI one, typically because your domain uses a different natural unit.

The fact that units can be added to the pre-built SI system is what makes the crate usable in a real codebase, since every field and measurement unit outside the SI set needs adding. The unit.rs example is the demonstration of that path.

The repository layout supports all of this: src/ for the library, uom-macros/ for the procedural macro that generates the types, examples/ for the four programs, tests/ for the test suite plus the two check crates, a Cargo.toml and Cargo.lock, a CHANGELOG.md, a CONTRIBUTING.md, a .rustfmt.toml for formatting, and both LICENSE-APACHE and LICENSE-MIT files at the root.

Where uom stops, and what to use instead

There are four honest limits, and they are all consequences of the design rather than gaps in it.

The first is the storage type constraint, which the README is upfront about. Normalising to base units means the value you can construct is bounded by what the storage type can represent, and the centimetre-in-an-i32 example is the clearest illustration. If your domain has values spanning many orders of magnitude, you need the bigint or rational types, and that has its own costs in compile time and in the size of the generated code.

The second is the macro weight. The quantity types are generated by a procedural macro, and Cargo.toml shows a workspace with the macro as a separate member, plus separate crates for the edition and feature checks. Macro-generated types of this size cost compile time, and the crate's own CI has a workflow named for a full test suite, which is a hint at the matrix of feature combinations that has to be kept working. For a large crate with many quantities, that compile-time bill is real, and the README does not quote figures for it.

The third is the boundary of the type system itself. uom checks that you add length to length and not length to time. It does not check that a length is a plausible length, that a time is non-negative, or that a value is within a physical range. Those are runtime concerns that the crate leaves to you, and no amount of dimensional checking substitutes for a range check on a physical input.

The fourth is that the quantity model is a closed set per system. To use a unit that is not in the SI system you either add it through the documented extension path or define your own system. There is no runtime plugin mechanism, and there should not be, since the whole design depends on the types being known at compile time.

The alternative to compare against is a runtime library, one that wraps a value and a unit in a struct and checks at runtime, or one that uses a floating point value plus a unit enum and returns a Result on mismatch. Those are easier to adopt incrementally, they work with dynamic unit sets, and they cost runtime overhead and produce runtime errors instead of compile errors. The difference in approach is exactly the difference between a quantity and a value-with-a-unit, and the choice between them is a choice about whether you want the mistake caught at compile time or at run time.

Editorial conclusion

Adopt uom if you are writing Rust code where mixing up units has real consequences, in engineering, physics or simulation work, and you want the compiler to reject length plus time rather than relying on a review convention. Do not adopt it if the cost of normalising to base units and the compile-time weight of a macro-generated type system outweighs that safety, or if you need exact decimal arithmetic for values that do not fit the storage type you choose. Verify first that the storage type you want can represent the range and precision your quantities need, because uom normalises to base units and cannot represent values it cannot store, and that the crate version you pin matches the rust-version your toolchain satisfies, which is 1.68.0.

Frequently asked questions

What is uom and how is it different from a units library?

uom is a Rust crate for automatic type-safe zero-cost dimensional analysis. Rather than working with measurement units, it works with quantities such as length, mass and time, so units only appear at interface boundaries and operations have zero runtime cost over the raw storage type.

How do I add uom to a Rust project?

uom requires rustc 1.68.0 or later. Add `uom = "0.38.0"` to the dependencies in your Cargo.toml and add `extern crate uom;` to your crate root. The example uses the SI system with `use uom::si::f32::*;` and constructs values such as Length::new::<kilometer>(5.0).

Does uom support no_std and Serde?

Both are available as Cargo features. Disabling the std feature compiles uom with no_std, and it is enabled by default so you have to opt out, while the serde feature enables serialization and deserialization of quantities with the Serde crate and is disabled by default.

What are the limits of uom's storage types?

Because values are normalized to the base unit, some values cannot be represented or precisely represented for floating point and rational storage types. The README's example is that with length based on meter and an i32 storage type, 1 centimeter cannot be represented, since it normalizes to 0.01 meter.

Official sources

  1. iliekturtles/uom on GitHub
  2. Issues
  3. License: Apache-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/iliekturtles-uom.svg)](https://hysenlabs.com/projects/iliekturtles-uom)

Was this useful?