danwi 0.4.2

Zero-cost dimensional analysis library with SI units, compile-time checking, and no_std support
Documentation
use danwi::{
    UnitDef,
    prelude::*,
    unit::{prefix::Kilo, si::Gram},
};

fn assert_close(a: f64, b: f64) {
    assert!((a - b).abs() <= 1e-12 * b.abs().max(1.0), "{a} != {b}");
}

#[test]
fn construction_forms_agree() {
    let v = 10.0_f64 * V;
    assert_eq!(v, 10.0.V());
    assert_eq!(v, 10.0.volt());
    assert_eq!(v, Volt::from(10.0));
    assert_eq!(v, Volt::new(10.0, V));
    assert_eq!(v, Volt::new(10_000.0, mV));
    assert_eq!(v.value(), 10.0);
}

#[test]
fn prefixed_arithmetic() {
    let v = (5.0_f64 * mA) * (2.0_f64 * kOhm);
    assert_eq!(v, 10.0 * V);
    assert_eq!(v.to(mV), 10_000.0);
    assert_eq!(v.to(kV), 0.01);

    let i = v / 2.0.kOhm();
    assert_eq!(i, 5.0.mA());

    let t = 1.0.s() + 1e3.ms() + 1e6.us() + 1e9.ns();
    assert_eq!(t, 4.0.second());
    assert_eq!(t / 2.0.s(), 2.0);
}

#[test]
fn kilogram_is_the_coherent_mass_unit() {
    // F = m·a with F in newtons must give mass in kilograms, not grams.
    let mass = 1.0.N() / 1.0.mps2();
    assert_eq!(mass.to(kg), 1.0);
    assert_eq!(mass.to(g), 1_000.0);
    assert_eq!(mass, 1.0.kg());

    // The kilo·(1/1000) scale must fold away completely at the type level.
    assert_eq!(<Kilo<Gram> as UnitDef>::SCALE_NUM, 1);
    assert_eq!(<Kilo<Gram> as UnitDef>::SCALE_DEN, 1);
}

#[test]
fn celsius_offset() {
    let t = 25.0_f64 * degC;
    assert_eq!(t.value(), 298.15); // stored in kelvin
    assert_eq!(t.to(K), 298.15);
    assert_eq!(t.to(degC), 25.0);
    assert_eq!(t, 25.0.celsius());
    assert_eq!((0.0_f64 * degC).to(K), 273.15);
}

#[test]
fn non_decimal_scales() {
    assert_eq!(1.5.hour(), 90.0.minute());
    assert_eq!(1.0.minute(), 60.0.s());
    assert_eq!((1.0_f64 * h).to(min), 60.0);

    assert_close((1.0_f64 * inch).to(mm), 25.4);
    assert_close((12.0_f64 * inch).value(), 0.3048); // one foot in metres

    assert_eq!(1.0.liter(), 1_000.0.mL());
    assert_eq!((1.0_f64 * L).value(), 0.001); //
    assert_eq!(1.0.bar(), 100.0.kPa());
    assert_eq!(1.0.bar().to(Pa), 100_000.0);
}

#[test]
fn reciprocal_dimensions() {
    let period = 1.0.s();
    let freq = 1.0 / period;
    assert_eq!(freq, 1.0.Hz());
    assert_eq!(freq, 1.0.hertz());
}

#[test]
fn display() {
    let v = 10.5_f64 * kV;
    assert_eq!(format!("{}", v.display_as(kV)), "10.5 kV");
    assert_eq!(format!("{}", v.display_as(V)), "10500 V");
    assert_eq!(format!("{:.1}", v.display_as(MV)), "0.0 MV");

    assert_eq!(format!("{}", (25.0_f64 * degC).display_as(degC)), "25 °C");
    assert_eq!(format!("{}", (2.0_f64 * kOhm).display_as(kOhm)), "2 kΩ");
    assert_eq!(format!("{}", 3.0.mps().display_as(mps)), "3 m/s");
    assert_eq!(format!("{}", 3.0.kmps().display_as(kmps)), "3 km/s");

    // Bare `Display` prints the base-unit value without a symbol.
    assert_eq!(format!("{}", 1.5_f64 * kV), "1500");
}

#[test]
fn readme_mixed_units() {
    let a = Meter::from(100.0);
    let b = 50.0 * cm;
    let c = 0.001 * km;
    let len = a + b + c;
    assert_eq!(len.value(), 101.5);
    assert_eq!(format!("{}", len.display_as(cm)), "10150 cm");
}

#[test]
fn scalar_ops_and_negation() {
    assert_eq!(3.0.V() - 1.0.V(), 2.0.V());
    assert_eq!(-(2.0.V()), Volt::from(-2.0));
    assert_eq!(2.0.V() * 3.0, 6.0.V());
    assert_eq!(3.0 * 2.0.V(), 6.0.V());
    assert_eq!(6.0.V() / 3.0, 2.0.V());
}

#[test]
fn dimensionless_into_scalar() {
    assert_eq!(5.0.V() / 5.0.V(), 1.0);

    let ratio = 4.0.s() / 2.0.s();
    let x: f64 = ratio.into();
    assert_eq!(x, 2.0);

    fn takes_f64(x: f64) -> f64 {
        x
    }
    assert_eq!(takes_f64((10.0.V() / 5.0.V()).into()), 2.0);
}

#[test]
fn ordering() {
    assert!(1.0.mV() < 1.0.V());
    assert!(2.0.kOhm() > 500.0.Ohm());
    assert!(1.0.V() <= 1.0.V());

    // dimensionless quantities compare against raw scalars, both ways
    let ratio = 4.0.s() / 2.0.s();
    assert_eq!(ratio, 2.0);
    assert_eq!(2.0, ratio);
    assert!(ratio < 3.0);
    assert!(3.0 > ratio);
}

/// Coherent SI relations: each derived unit equals the product of its
/// defining units, with no scale factor.
#[test]
fn physics_identities() {
    assert_eq!(1.0.kg() * 1.0.mps2(), 1.0.N());
    assert_eq!(1.0.N() * 1.0.m(), 1.0.J());
    assert_eq!(1.0.J() / 1.0.s(), 1.0.W());
    assert_eq!(1.0.N() / (1.0.m() * 1.0.m()), 1.0.Pa());
    assert_eq!(1.0.m() / 1.0.s(), 1.0.mps());
    assert_eq!(1.0.mps() / 1.0.s(), 1.0.mps2());

    assert_eq!(1.0.V() * 1.0.A(), 1.0.W());
    assert_eq!(1.0.A() * 1.0.s(), 1.0.C());
    assert_eq!(1.0.V() / 1.0.A(), 1.0.Ohm());
    assert_eq!(1.0 / 1.0.Ohm(), 1.0.S());
    assert_eq!(1.0.C() / 1.0.V(), 1.0.F());
    assert_eq!(1.0.V() * 1.0.s(), 1.0.Wb());
    assert_eq!(1.0.Wb() / (1.0.m() * 1.0.m()), 1.0.T());
    assert_eq!(1.0.Wb() / 1.0.A(), 1.0.H());
}

/// Construct-then-read across the prefix ladder. Positive prefixes are
/// exact (`x * C` then `/ C` divides a value by itself); negative prefixes
/// construct by division, which can round one ulp low.
#[test]
fn prefix_ladder_roundtrips() {
    fn ulp(a: f64) {
        assert!((a - 1.0).abs() <= f64::EPSILON, "{a} != 1.0 within 1 ulp");
    }

    assert_eq!(1.0.QV().to(QV), 1.0);
    assert_eq!(1.0.RV().to(RV), 1.0);
    assert_eq!(1.0.YV().to(YV), 1.0);
    assert_eq!(1.0.ZV().to(ZV), 1.0);
    assert_eq!(1.0.EV().to(EV), 1.0);
    assert_eq!(1.0.PV().to(PV), 1.0);
    assert_eq!(1.0.TV().to(TV), 1.0);
    assert_eq!(1.0.GV().to(GV), 1.0);
    assert_eq!(1.0.MV().to(MV), 1.0);
    assert_eq!(1.0.kV().to(kV), 1.0);
    assert_eq!(1.0.hV().to(hV), 1.0);
    assert_eq!(1.0.daV().to(daV), 1.0);
    assert_eq!(1.0.V().to(V), 1.0);

    ulp(1.0.dV().to(dV));
    ulp(1.0.cV().to(cV));
    ulp(1.0.mV().to(mV));
    ulp(1.0.uV().to(uV));
    ulp(1.0.nV().to(nV));
    ulp(1.0.pV().to(pV));
    ulp(1.0.fV().to(fV));
    ulp(1.0.attoV().to(attoV));
    ulp(1.0.zV().to(zV));
    ulp(1.0.yV().to(yV));
    ulp(1.0.rV().to(rV));
    ulp(1.0.qV().to(qV));

    assert_eq!((1.0_f64 * QV).value(), 1e30);
    assert_eq!(1.0.kV().to(mV), 1_000_000.0);
}

#[test]
fn name_form_methods() {
    assert_eq!(1.0.kilovolt(), 1.0.kV());
    assert_eq!(1.0.millisecond(), 1.0.ms());
    assert_eq!(1.0.kilogram(), 1.0.kg());
    assert_eq!(1.0.microvolt(), 1.0.uV());
    assert_eq!(1.0.attosecond(), 1.0.attos());
    assert_eq!(1.0.meter_per_second(), 1.0.mps());
}

#[test]
fn debug_formats() {
    assert_eq!(format!("{:?}", 2.5.V()), "Quantity(2.5)");
    assert_eq!(format!("{:?}", kV), "Unit(kV)");
    assert_eq!(format!("{:?}", degC), "Unit(°C)");
}

#[cfg(feature = "f32")]
#[test]
fn f32_support() {
    use danwi::f32::{QuantityExt, constants::*, types};

    fn close(a: f32, b: f32) {
        assert!((a - b).abs() <= 1e-5 * b.abs().max(1.0), "{a} != {b}");
    }

    let v: types::Volt = 5.0_f32.kV();
    assert_eq!(v.to(V), 5_000.0_f32);
    assert_eq!(5.0_f32.mA() * 2.0_f32.kOhm(), 10.0_f32.V());
    assert_eq!(format!("{}", v.display_as(kV)), "5 kV");
    assert!(1.0_f32.mV() < 1.0_f32.V());

    // f32 can't roundtrip the 273.15 offset exactly
    close(25.0_f32.celsius().to(degC), 25.0);
    close(25.0_f32.celsius().to(K), 298.15);
}

#[test]
fn standard_gravity_constant() {
    assert_eq!(danwi::f64::G_0, 9.80665.mps2());
}