Hi all! I've published qty, a crate for physical quantities checked by the compiler. A length, a duration and a speed are different types, and the arithmetic between them is checked at compile time:

use qty::*; let distance = Length::from_kilometers(10.0); let time = Duration::from_minutes(30.0); let speed: Speed = distance / time; assert_eq!(format!("{:.1}", speed.in_unit(SpeedUnit::MilePerHour)), "12.4 mph"); // distance + time doesn't compile 

The quantities and units are generated from the definitions of UnitsNet, the .NET units library: 128 quantities and about 1,700 units, from parsecs to US survey feet. The generated tests check every unit against the upstream conversion formulas.

How it works

Each quantity is a #[repr(transparent)] newtype over one number in its base unit (Length holds metres), so it has the same size as that number and compiles to the same code. Converting to another unit is a single multiplication. Relations between quantities (force = mass × acceleration, energy = power × time, …) are plain operator impls, generated and checked against the SI dimensions.

If you know uom qty takes the other side of that trade-off. Each quantity is a concrete type rather than a type-level combination of dimensions. That keeps the types and error messages simple, but a product that isn't a named quantity, like length × mass, doesn't exist.

Other things it does

  • Text in both directions: "5 kilometres".parse::<Length>(), "3 kg*m/s^2".parse::<Force>(), and auto_unit() to print 2.5 mm instead of 0.0025 m.
  • Temperatures are points on a scale: subtracting two gives a TemperatureDelta, and adding two doesn't compile.
  • Fixed<Millimeter, i32> counts whole units in an integer, for sensor data and file formats. Exact operations just work. Anything that would round asks for a rounding mode.
  • no_std without allocation, and no required dependencies. Optional serde (as plain numbers, or as text like "30 s"), approx, bytemuck, defmt, chrono, time and schemars.
  • Quantity groups are features, so building only the core quantities takes about a fifth of the time.

Supports your own units and quantities

custom_unit! adds a unit to an existing quantity (a furlong for Length), and custom_quantity! defines a whole new one:

custom_quantity! { // Printed paper, counted in sheets. pub struct Paper; pub enum PaperUnit { Sheet: "sh", // the base unit Quire: "qr" = 25.0, // sheets in a quire Ream: "rm" = 500.0, } } let order = Paper::new(3.0, PaperUnit::Ream) + "2 quires".parse::<Paper>().unwrap(); assert_eq!(format!("{:.1}", order.in_unit(PaperUnit::Ream)), "3.1 rm"); 

Feedback welcome

I'd especially like to hear:

  • whether the API reads naturally in your domain, and which quantities or units you miss;
  • if you'd use custom quantities: whether they need to parse unit expressions ("kg/m²") or work with Fixed, which they don't yet;
  • anything that's awkward with f32, integers or your own number types.

crates.io - GitHub

1 post - 1 participant

Read full topic