#unit-conversion #calcscript #unit #convert #unit-convert

cas-unit-convert

Unit conversion library for CalcScript

1 unstable release

new 0.1.0 Apr 17, 2025

#565 in Math

MIT license

33KB
561 lines

A module for generic conversion between units.

Usage

The Measurement type packs a numeric value and the unit it represents. It can be converted to other units within the same quantity kind. Here's an example of converting a length from miles to decimeters:

use cas_unit_convert::{Base, Length, Measurement, Unit};

let m = Measurement::new(2.0, Unit::new(Base::Length(Length::Mile)));
let m2 = m.convert(Unit::new(Base::Length(Length::Decimeter))).unwrap();
assert_eq!(m2.value(), &32186.88);

Note that the arguments to Measurement::new and Measurement::convert accept any type that implements Into<Unit>, which is implemented for all provided units and quantities. This allows you to write the above example more concisely:

use cas_unit_convert::{Length, Measurement};

let m = Measurement::new(2.0, Length::Mile);
let m2 = m.convert(Length::Decimeter).unwrap();
assert_eq!(m2.value(), &32186.88);

Derived units

There is rudimentary support for derived units, which are units that are defined in terms of other units. For example, area is defined in terms of length squared, and volume is defined in terms of length cubed, and you can convert between them. The .squared(), .cubed(), and .pow() methods on Units and Bases allow you to create derived units easily:

use cas_unit_convert::{Area, Length, Measurement};

let m = Measurement::new(57600.0, Length::Foot.squared());
let m2 = m.convert(Area::Acre).unwrap();
assert_eq!(m2.value(), &1.322314049586777);

Additionally, compound units are supported, such as m/s (meters per second) and kg*m/s^2 (kilogram meters per second squared, aka Newtons):

use cas_unit_convert::{CompoundUnit, Measurement, Unit};

// easiest way to create these is with string parsing
let m = Measurement::new(30.0, "mi/hr".parse::<CompoundUnit>().unwrap());
let m2 = m.convert("km/s".parse::<CompoundUnit>().unwrap()).unwrap();
assert_eq!(m2.value(), &0.0134112);

// it can also be done manually with `CompoundUnit`

Note on derived units

It is possible to declare a compound unit with two units of the same type, such as "mimi" or even "mikm". This behavior is currently unsupported, and while it won't cause undefined behavior (no unsafe code), it can lead to strange results.

Rounding errors

Floating point arithmetic is inherently imprecise; there are infinitely many real numbers, but only so many floating point numbers to represent them. As a result, small, but noticeable rounding errors can occur when converting between units.

In general, you should not use == to compare floating point numbers. Instead, use a function that checks for approximate equality (through subtraction and comparison with a small epsilon). Crates that provide this functionality include assert_float_eq (with assertions) and approx (for general use).

This example below will fail, even though the conversion is mathematically correct:

use cas_unit_convert::{Length, Measurement, Volume};

let m = Measurement::new(1.0, Length::Centimeter.cubed());
let m2 = m.convert(Volume::Milliliter).unwrap();
assert_eq!(m2.value(), &1.0); // panics! (the result is 1.0000000000000002)

Instead, here's a better way to write the test, using assert_float_eq:

#[macro_use]
extern crate assert_float_eq;

use cas_unit_convert::{Length, Measurement, Volume};

# fn main() {
let m = Measurement::new(1.0, Length::Centimeter.cubed());
let m2 = m.convert(Volume::Milliliter).unwrap();
assert_float_relative_eq!(*m2.value(), 1.0);
# }

If you need to compare floating point numbers outside of a test, you can use approx:

use approx::abs_diff_eq;
use cas_unit_convert::{Length, Measurement, Volume};

let m = Measurement::new(1.0, Length::Centimeter.cubed());
let m2 = m.convert(Volume::Milliliter).unwrap();
if !abs_diff_eq!(*m2.value(), 1.0) {
    // this will not panic. we're safe!
    panic!("1 centimeter cubed is not equal to 1 milliliter");
}

In practice, the precision errors are usually small enough that this is not a problem, but it's something you should be aware of.

Demo

This repository includes a simple CLI tool to convert units using this library. To run it, run the following command from the root of the repository:

cargo run --example convert-unit-repl

This will start a REPL where you can enter conversion commands. Commands are given in the form <value> <from unit> <to unit>. For example:

> 1.5 yd ft
1.5 yd = 4.5 ft
> 500 mL cm^3
500 mL = 500 cm^3
> 5.75 g/mL lb/gal
5.75 g/mL = 47.986... lb/gal

No runtime deps