uom icon indicating copy to clipboard operation
uom copied to clipboard

Make methods on Quantity more generic

Open DusterTheFirst opened this issue 3 years ago • 4 comments

Currently, the Quantity struct has implemented a few methods like new and format_args defined on (what looks like) the given dimensions for that system of measurements

Sample methods defined on the struct

image

I am writing a struct generic over any quantity in the SI system of units:

The generic struct
pub type Quantity<T> = uom::si::Quantity<T, SI<f64>, f64>;

pub struct Vector3<T: Dimension + ?Sized> {
    x: Quantity<T>,
    y: Quantity<T>,
    z: Quantity<T>,
}

impl<T: Dimension + ?Sized> Debug for Vector3<T> {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("Vector3")
            .field("x", &Quantity::format_args(self.x, Abbreviation)) // Same error over T:
            .field("y", &Quantity::format_args(self.xy Abbreviation))
            .field("z", &Quantity::format_args(self.z, Abbreviation))
            .finish()
    }
}

impl<T: Dimension + ?Sized> Vector3<T> {
    pub fn new(x: Quantity<T>, y: Quantity<T>, z: Quantity<T>) -> Self {
        Self { x, y, z }
    }

    pub fn zero() -> Self {
        Self {
            x: Quantity::new(), // Or T::new(), neither work
            y: Quantity::new(),
            z: Quantity::new(),
        }
    }
}

but am running into problems since the new and format_args methods are only defined on the specific Dimension types that were (maybe) defined in the macro invocation which makes it almost impossible to use the Quantity type in a generic context.

The errors

image

A solution I had that I wanted to propose before attempting would be to make a trait that is implemented for each dimension versus new methods implemented directly on the struct so that the Quantity as long as it implements the trait. Something like:

trait SIQuantity {
// Here new and all those methods would be layed out
}

impl SIQuantity for Quantity<specific_dimension, ....> {
// Per dimention implementation
}

which would allow the above code to become:

mpl<T: Dimension + ?Sized> Debug for Vector3<T>
where
    Quantity<T>: SIQuantity,
{
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        f.debug_struct("Vector3")
            .field("x", &Quantity::format_args(self.x, Abbreviation))
            .field("y", &Quantity::format_args(self.xy Abbreviation))
            .field("z", &Quantity::format_args(self.z, Abbreviation))
            .finish()
    }
}

I have skimmed through the source code and tried to implement it myself, but I am not familiar enough with the code base to make anything useful.

DusterTheFirst avatar Dec 19 '20 01:12 DusterTheFirst

I might also just be using the crate wrong

DusterTheFirst avatar Dec 19 '20 01:12 DusterTheFirst

This is definitely a point that isn't ideal and I haven't put enough effort into finding a great solution yet. Currently Quantity's fields are public so you can use the type constructor:

// Gravitational constant.
const G: Quantity<uom::si::ISQ<P3, N1, N2, Z0, Z0, Z0, Z0>, uom::si::SI<f64>, f64> = Quantity { dimension: PhantomData, units: PhantomData, value: 6.674e-11, };

// Based on your generic example.
let x = Quantity<T, uom::si::SI<f64>, f64> = Quantity { dimension: PhantomData, units: PhantomData, value: 6.674e-11, };

Edit: leaving this open rather than re-opening #28. I like to make the fields internal eventually and have a better solution that doesn't rely on exposing the struct details.

iliekturtles avatar Dec 19 '20 19:12 iliekturtles

Yah, thats what I have started to do. Thanks for all your work on it its such a great crate.

DusterTheFirst avatar Dec 20 '20 20:12 DusterTheFirst

I have a minimal use case to illustrate the need to use methods defined on Quantity in a generic way.

Let's say that we want to make a generic formatter for some application that can format a quantity and validate user input. The following is a non-generic example using specifically Pressure as quantity type and megapascal for formatting. Currently, it seems that traits are not shared between types of quantities or units, so it is not possible to turn Pressure or megapascal into generic parameters.

use uom::fmt::DisplayStyle::Abbreviation;
use uom::si::f64::Pressure;
use uom::si::pressure::{kilopascal, megapascal};
use uom::str::ParseQuantityError;

struct UnitFormatter;

impl UnitFormatter {
    fn validate(&self, input: &str)
                -> std::result::Result<Pressure, ParseQuantityError> {
        input.parse::<Pressure>()
    }
    
    fn format(&self, value: &Pressure) -> String {
        format!("{}", value.into_format_args(megapascal, Abbreviation))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_unit_formatter() {
        let v = UnitFormatter { };
        let p = Pressure::new::<kilopascal>(1000.0);
        assert_eq!(v.format(&p), "1 MPa");
        assert_eq!(v.validate("1 MPa").unwrap(), p);
    }
}

emgstabilis avatar Jan 18 '22 09:01 emgstabilis