Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Operations for Quantity references #307

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from

Conversation

jacg
Copy link
Contributor

@jacg jacg commented May 25, 2022

Fix #38.

Prototype implementations of

  1. &q +/- q
  2. q +/- &q
  3. &q +/- &q

Could you check that I'm barking up the right tree, before I try to complete the rest?

@iliekturtles
Copy link
Owner

This is definitely moving in the right direction. My biggest concern so far is duplicate code. The code base already has near-duplicate implementations because of autoconvert!. Can these be merged? When references are added does this force us to quadruple the number of implementations? I did some digging to find the standard library macro forward_ref_binop which does have four separate implementations. This makes me think we'll need at least four near-duplicate implementations.

Next is that some of the underlying storage types such as bigint aren't Copy. Will that force another doubling or can it be worked around?

Finally should we be trying to forward to the same operator? e.g. impl Add<&Q> for &Q is actually using impl Add<V> for V in the autoconvert! blocks because self.value is passed by value and change_base returns a new instance of V.

@jacg
Copy link
Contributor Author

jacg commented Jun 16, 2022

Apologies for the long delay, my time to work on this is sporadic.

I agree, the code duplication is quite horrendous. When I try to make sense of it, I get lost and my eyes glaze over.

I've taken the liberty of summarizing your comment, broken down into enumerated questions:

  1. Can autoconvert! duplication be merged?
  2. Does introducing references lead to quadruple code repetition?
  3. Do Copy vs nonCopy underlying storage types introduce further doubling?
  4. Should we forward to same operator?

Faced with all of these at once, I don't know where to begin. Do you think we could identify one of them as a candidate on which to start making some progress?

@iliekturtles
Copy link
Owner

I've been doing some investigation and believe we can eliminate all of the duplication, include the existing duplication for autoconvert. Since 1.13.0 macros are allowed in the type position. This wasn't the case when the original stdlib code was written and I'm guessing no one went back to reduce duplication after that.

I wrote out some example code in the playground showing how type parameters and references can be inserted and I believe we can put together a recursive macro as follows:

  1. Outer call injects which units type parameters should be used.
#[cfg(not(feature = "autoconvert"))]
impl_ops!(U);

#[cfg(feature = "autoconvert")]
impl_ops!(Ul, Ur);
  1. First branch injects the by-val/by-ref combinations. This could possibly be merged with the outer call since there really isn't much duplication. This may also need to be merged with the previous step because $() repetitions that aren't nested in the pattern can't be nested in the body. This would essentially mean hand-writing the 8 combinations (e.g. ((U), V, V), ((Ul, Ur), V, V), ((U), V, &V), ((Ul, Ur), V, &V), ...).
 macrol_rules! impl_ops {
    ($(U:ty),+) => {
        impl_ops!(@types ($($U),+), ((V, V), (V, &V), (&V, V), (&V, &V));
    };
}
  1. Second branch injects the traits to implement. This is basically the original code with the U and V information added in.
 macrol_rules! impl_ops {
    (@types ($($U:ty),+), ($(($Vl:ident, $Vr:ident)),+)) => {
        impl_ops!(@impl ($($U),+), ($(($Vl, $Vr)),+), Add, add, +, ...);
        impl_ops!(@impl ($($U),+), ($(($Vl, $Vr)),+), Sub, sub, -, ...);
    };
}
  1. Innermost branch has the actual implementations. Note how usages of U, Ur, Ul, and V are replaced by macro calls to transform them into the appropriate actual type.
 macrol_rules! impl_ops {
    (@types ($($U:ty),+), ($(($Vl:ident, $Vr:ident)),+), ...) => {

        impl<D, $(U,)+, V> $AddMul<Quantity<D, impl_ops!(@Ur $($U),+), impl_ops!(@Vr $($Vl, $Vr),+)> for ...
    };
}
  1. Finally, extra match arms without repetitions can do the actual transformation.
 macrol_rules! impl_ops {
    (@Ul $U:ty) => { $U };
    (@Ul $Ul:ty, $Ur:ty) => { $Ul };
    (@Ur $U:ty) => { $U };
    (@Ur $Ul:ty, $Ur:ty) => { $Ur };
    (@Vl $Vl:ident, $Vr:ident) => { $Vl };
    (@Vr $Vl:ident, $Vr:ident) => { $Vr };
}

Let me know how you want to proceed and how I can assist. You might want to start out with a simple playground or test project and use cargo expand to view the output.

@jacg
Copy link
Contributor Author

jacg commented Jul 15, 2022

So you can think of the whole recursive macro invocation graph as a tree, with macro invocations at the nodes, and actual code (impls) as the leaves.

I think that the way forward is to start at the bottom node-level, and manually invoke the macro to generate a bunch of related leaves. Then repeat it for some siblings of the node, to make sure the pattern is consistent. And then try to go up one level, and replace the manual invocations at the lowest node level, with ones generated by a manual invocation at the higher level. And so on, until the whole lot is replaced with one big top-level invocation.

This should make it possible to gradually replace (and fill gaps in) the existing code while keeping the whole lot compiling throughout.

Does this sound reasonable? Can you see any obvious flaws in this strategy? Can you think of a good candidate for a first bottom node to try?

@iliekturtles
Copy link
Owner

That sounds like a good way to start to me. I would just make sure that you include all the parameters that will eventually be needed ($($U), $Vl, $Vr).

macro_rules! impl_ops {
    (($($U:ident),+), $Vl:ty, $Vr:ty, $AddSubTrait:ident, ...) => {
        impl<D, $($U,)+ V> $crate::lib::ops::$AddSubTrait<Quantity<D, impl_ops!(@Ur $($U),+), $Vr>> for Quantity<D, impl_ops!(@Ul $($U),+), $Vl> {
            // ...
        })+
    }

@jacg
Copy link
Contributor Author

jacg commented Jul 16, 2022

Could you help me make sense of the existing implementations?

Looking at the original code, I see the following nine impls:

lines ops autoconv AddSub MulDiv
1 394-414 +- ✔️ Ul,Ur D ✔️
2 416-434 +- U D ✔️
3 436-451 +- = ✔️ Ul,Ur D ✔️
4 453-466 +- = D ✔️
5 468-494 */ ✔️ Ul,Ur Dl,Dr ✔️ ✔️
6 496-521 */ U Dl,Dr ✔️ ✔️
7 523-540 */ U D ✔️ dimensionless RHS?
8 542-554 */ = U D ✔️ dimensionless RHS?
9 556-587 */ U D ✔️ ✔️ in a mod, Z0

The first four only use add-sub related names, and cover the four possible combinations of assignment-autoconversion. This all makes sense.

The next four are trickier:

  • Even though they implement mul-div, they use add-sub names for type-level arithmetic on the dimensions.
  • The assign versions cannot work in general, only when the RHS is a Ratio or some plain float.
  • The two that don't use add-sub names (7 & 8) must be covering the RHS-is-dimensionless case (once with =, once without), but I don't see how that constraint is expressed. There is a typenum::Z0 in no. 9, but not here.

Then there is a ninth impl inside a module. What's that? I see that it's the only one which has a typenum::Z0 constraint.

I would have expected autoconversion only to make sense in the presence of Ul, Ur, but 7, 8 and 9 are counterexamples.

@iliekturtles
Copy link
Owner

  • When you add or subtract quantities the dimensions stay the same. e.g. length + length = length. To write in terms of quantity Quantity<D, U, V> + Quantity<D, U, V> = Quantity<D, U, V>. The D parameter does not change. When you multiply or divide the dimensions change. e.g. length / time = velocity. Written in terms of quantity Quantity<Dl, U, V> / Quantity<Dr, U, V> = Quantity<Doutput, U, V>. To calculate the this change for multiplication or division the powers of the base quantities are added or subtracted: length (L¹T⁰) / time (L⁰T¹) = length (L¹T⁰) * frequency (L⁰T⁻¹) = velocity (L¹⁺⁰T⁰⁺⁻¹).

  • Why can't the assign versions work in general? Because they only have a single U parameter? Instead of using the enumeration of U parameters in the macro, just use Ul (Or Ur for 9).

  • 7 and 8 implement multiply and divide when RHS is a scalar (plain number, e.g. f32) and maintain the dimensions. e.g. Quantity<D, U, V> */ f32 = Quantity<D, U, V>. 9 implements multiply and divide when LHS is a scalar and RHS is a quantity. e.g. f32 */ Quantity<D, U, V>. You can think of this impl as if it were f32.into_ratio_quantity() */ Quantity<D, U, V> and then look at the definition for 5 replacing Dl with a dimension where all base quantities are Z0 and Ul with Ur. For multiply this will maintain the dimensions and scale the value. For divide this will essentially invert the quantity. e.g. 1 / time = frequency.

    The mod is used because the impl<D, U> doesn't introduce a V parameter (would be an orphan rule violation) so the storage_types! macro is used to define a V type alias that can be used. storage_types! also introduces new mod names so the outer mod effectively hides them so they don't cause conflicts with later uses of storage_types!.

@jacg
Copy link
Contributor Author

jacg commented Aug 15, 2022

I have realistic hope of being able to spend some time on this, this week.

I've been wondering about what you wrote here:

I wrote out some example code in the playground showing how type parameters and references can be inserted

I understand how these macro tricks work, but I don't understand why we need them. What does the version on the first line here

impl<X, Y> T<x!(@R X, Y)> for S1<x!(@P X, Y)> {}
impl<X, Y> T<         Y > for S2<     &X    > {}

(playground) give us, that the version on the second line does not (apart from noise and obfuscation)?

Perhaps I'm missing something about how this information will propagate from the top of the macro call, but out of that context it seems that the first line uses x!(@R X, Y) where it could simply say Y, and x!<@P X, Y> as a long-winded and cryptic way of saying &X.

Also, I've been wondering whether impl_ops should be a single recursive macro with lots of match arms playing a variety of different roles corresponding to different levels of the invocation tree, or whether it wouldn't be better to have different macros for each level, or maybe even have different macros for different sub-trees at the same level.

@jacg
Copy link
Contributor Author

jacg commented Aug 15, 2022

Do you have any words of wisdom on how to test this?

I've found

uom/src/tests/quantity.rs

Lines 54 to 129 in d55d64a

#[cfg(feature = "autoconvert")]
quickcheck! {
#[allow(trivial_casts)]
fn add(l: A<V>, r: A<V>) -> TestResult {
let km: V = <kilometer as crate::Conversion<V>>::coefficient().value();
let f = (&*l + &*r) / &km;
let i = (&*l / &km) + (&*r / &km);
if !Test::approx_eq(&i, &f) {
return TestResult::discard();
}
TestResult::from_bool(
Test::approx_eq(&k::Length::new::<meter>(&*l + &*r),
&(k::Length::new::<meter>((*l).clone())
+ f::Length::new::<meter>((*r).clone()))))
}
#[allow(trivial_casts)]
fn sub(l: A<V>, r: A<V>) -> TestResult {
let km: V = <kilometer as crate::Conversion<V>>::coefficient().value();
let f = (&*l - &*r) / &km;
let i = (&*l / &km) - (&*r / &km);
if !Test::approx_eq(&i, &f) {
return TestResult::discard();
}
TestResult::from_bool(
Test::approx_eq(&k::Length::new::<meter>(&*l - &*r),
&(k::Length::new::<meter>((*l).clone())
- f::Length::new::<meter>((*r).clone()))))
}
#[allow(trivial_casts)]
fn mul_quantity(l: A<V>, r: A<V>) -> TestResult {
let km: V = <kilometer as crate::Conversion<V>>::coefficient().value();
TestResult::from_bool(
Test::approx_eq(&/*Area::new::<square_meter>*/(&*l * &*r),
&(f::Length::new::<meter>((*l).clone())
* k::Length::new::<meter>((*r).clone())).value)
&& Test::approx_eq(
&/*Area::new::<square_kilometer>*/(&(&*l / &km) * &(&*r / &km)),
&(k::Length::new::<meter>((*l).clone())
* f::Length::new::<meter>((*r).clone())).value)
&& Test::approx_eq(&/*Length-mass*/(&*l * &*r),
&(f::Length::new::<meter>((*l).clone())
* k::Mass::new::<kilogram>((*r).clone())).value)
&& Test::approx_eq(&/*Length-mass*/(&*l * &*r),
&(k::Length::new::<kilometer>((*l).clone())
* f::Mass::new::<kilogram>((*r).clone())).value))
}
#[allow(trivial_casts)]
fn div_quantity(l: A<V>, r: A<V>) -> TestResult {
if *r == V::zero() {
return TestResult::discard();
}
let km: V = <kilometer as crate::Conversion<V>>::coefficient().value();
TestResult::from_bool(
Test::approx_eq(&/*Ratio::new::<ratio>*/(&*l / &*r),
&(f::Length::new::<meter>((*l).clone())
/ k::Length::new::<meter>((*r).clone())).value)
&& Test::approx_eq(&/*Ratio::new::<ratio>*/(&*l / &*r),
&(k::Length::new::<meter>((*l).clone())
/ f::Length::new::<meter>((*r).clone())).value)
&& Test::approx_eq(&/*Length/mass*/(&*l / &*r),
&(f::Length::new::<meter>((*l).clone())
/ k::Mass::new::<kilogram>((*r).clone())).value)
&& Test::approx_eq(&/*Length/mass*/(&*l / &km / &*r),
&(k::Length::new::<meter>((*l).clone())
/ f::Mass::new::<kilogram>((*r).clone())).value))
}
and

uom/src/tests/system.rs

Lines 135 to 188 in d55d64a

#[allow(trivial_casts)]
fn add(l: A<V>, r: A<V>) -> bool {
Test::eq(&Length::new::<meter>(&*l + &*r),
&(Length::new::<meter>((*l).clone())
+ Length::new::<meter>((*r).clone())))
}
#[allow(trivial_casts)]
fn sub(l: A<V>, r: A<V>) -> bool {
Test::eq(&Length::new::<meter>(&*l - &*r),
&(Length::new::<meter>((*l).clone())
- Length::new::<meter>((*r).clone())))
}
#[allow(trivial_casts)]
fn mul_quantity(l: A<V>, r: A<V>) -> bool {
Test::eq(&/*Area::new::<square_meter>*/(&*l * &*r),
&(Length::new::<meter>((*l).clone())
* Length::new::<meter>((*r).clone())).value)
}
#[allow(trivial_casts)]
fn mul_v(l: A<V>, r: A<V>) -> bool {
Test::eq(&Length::new::<meter>(&*l * &*r),
&(Length::new::<meter>((*l).clone()) * (*r).clone()))
&& Test::eq(&Length::new::<meter>(&*l * &*r),
&((*l).clone() * Length::new::<meter>((*r).clone())))
}
#[allow(trivial_casts)]
fn div_quantity(l: A<V>, r: A<V>) -> TestResult {
if *r == V::zero() {
return TestResult::discard();
}
// TODO Use `.get(?)`, add ratio type?
TestResult::from_bool(
Test::eq(&(&*l / &*r),
&(Length::new::<meter>((*l).clone())
/ Length::new::<meter>((*r).clone())).value))
}
#[allow(trivial_casts)]
fn div_v(l: A<V>, r: A<V>) -> TestResult {
if *r == V::zero() {
return TestResult::discard();
}
TestResult::from_bool(
Test::eq(&Length::new::<meter>(&*l / &*r),
&(Length::new::<meter>((*l).clone()) / (*r).clone()))
&& Test::eq(&/*ReciprocalLength::new::<meter>*/(&*l / &*r),
&((*l).clone() / Length::new::<meter>((*r).clone())).value))
}
and some temperature-specifc ones here
#[cfg(test)]
mod tests {
use crate::si::quantities::*;
use crate::si::temperature_interval as ti;
use crate::si::thermodynamic_temperature as tt;
storage_types! {
use crate::tests::*;
use super::*;
quickcheck! {
#[allow(trivial_casts)]
fn add(l: A<V>, r: A<V>) -> bool {
Test::eq(&ThermodynamicTemperature::<V>::new::<tt::kelvin>(&*l + &*r),
&(ThermodynamicTemperature::<V>::new::<tt::kelvin>((*l).clone())
+ TemperatureInterval::<V>::new::<ti::kelvin>((*r).clone())))
}
#[allow(trivial_casts)]
fn sub(l: A<V>, r: A<V>) -> bool {
Test::eq(&ThermodynamicTemperature::<V>::new::<tt::kelvin>(&*l - &*r),
&(ThermodynamicTemperature::<V>::new::<tt::kelvin>((*l).clone())
- TemperatureInterval::<V>::new::<ti::kelvin>((*r).clone())))
}
}
}
mod non_big {
storage_types! {
types: PrimInt, Rational, Rational32, Rational64, Float;
use crate::tests::*;
use super::super::*;
quickcheck! {
#[allow(trivial_casts)]
fn add_assign(l: A<V>, r: A<V>) -> bool {
let mut f = *l;
let mut v = ThermodynamicTemperature::<V>::new::<tt::kelvin>(*l);
f += *r;
v += TemperatureInterval::<V>::new::<ti::kelvin>(*r);
Test::approx_eq(&ThermodynamicTemperature::<V>::new::<tt::kelvin>(f), &v)
}
#[allow(trivial_casts)]
fn sub_assign(l: A<V>, r: A<V>) -> bool {
let mut f = *l;
let mut v = ThermodynamicTemperature::<V>::new::<tt::kelvin>(*l);
f -= *r;
v -= TemperatureInterval::<V>::new::<ti::kelvin>(*r);
Test::approx_eq(&ThermodynamicTemperature::<V>::new::<tt::kelvin>(f), &v)
}
}
}
}
}

I have used Haskell's Quickcheck, many moons ago and I use Proptest in Rust, but I've never used Rust's Quickcheck, so these tests are a bit cryptic to me.

I'm tempted to write some very simple example-based tests, to try to make it blatantly obvious exactly what is being tested, and what isn't in each test. Even if we don't keep them, they should serve as a useful crutch during development.

@jacg
Copy link
Contributor Author

jacg commented Aug 15, 2022

IIUC

  • the autoconvert feature is enabled by cargo test,
  • the purpose of autoconvert is to enable mixing of base units in the arithmetic operations.

Consequently, I would expect variations on this theme to compile

    #[test]
    fn hmm() {
        use crate::si::time::second;
        let base_f32 = crate::si::f32::Time::new::<second>(1.0);
        let base_f64 = crate::si::f64::Time::new::<second>(1.0);
        base_f32 + base_f64;
    }

but I always get errors along these lines

error[E0308]: mismatched types
  --> src/tests/impl_ops.rs:55:20
   |
55 |         base_f32 + base_f64;
   |                    ^^^^^^^^ expected `f32`, found `f64`
   |
[...]

What am I doing wrong?

@iliekturtles
Copy link
Owner

Responding to your first comment:

I've been wondering about what you wrote here:

I wrote out some example code in the playground showing how type parameters and references can be inserted

I understand how these macro tricks work, but I don't understand why we need them. What does the version on the first line here

impl<X, Y> T<x!(@R X, Y)> for S1<x!(@P X, Y)> {}
impl<X, Y> T<         Y > for S2<     &X    > {}

(playground) give us, that the version on the second line does not (apart from noise and obfuscation)?

The intention was to parameterize if the reference on the type. When I was roughly writing out the example match arms in #307 (comment) I had it in my mind that the reference would be applied to the V parameter. This is not the case! The possible reference gets applied to Quantity. e.g. impl Add for Quantity<...> and impl Add for &Quantity<...>.

Here is another playground that I hope better illustrates what I believe needs to be done. To rephrase some of the earlier example code the parameterization should probably look something like this with the different U/Ul/Ur and reference combinations: ((U), , ), ((Ul, Ur), , ), ((U), , &), ((Ul, Ur), , &), ((U), &, ), ((Ul, Ur), &, ), ((U), &, &), ((Ul, Ur), &, &))

Let me know if this part makes sense. I'll respond to the rest of the questions later today or tomorrow.

@iliekturtles
Copy link
Owner

Also, I've been wondering whether impl_ops should be a single recursive macro with lots of match arms playing a variety of different roles corresponding to different levels of the invocation tree, or whether it wouldn't be better to have different macros for each level, or maybe even have different macros for different sub-trees at the same level.

I'm leaning towards a single macro with multiple "private" match arms.

Do you have any words of wisdom on how to test this?

I haven't thought about the best way to organize tests for all of the different combination of ops impls. For reference the first block you linked is for when units with different base units are operated on. The second is the main set of tests for the ops. The third is for temperature specific explicit implementations (certain ops are not implemented for thermodynamic temperature because of it's kind).

quickcheck! is essentially just calling the inner functions with random values in the parameters.

the purpose of autoconvert is to enable mixing of base units in the arithmetic operations.
but I always get errors along these lines

autoconvert allows for quantities with different base units (e.g. length stored as meter vs. length stored as kilometer) not different underlying storage types (f32, f64).

@jacg
Copy link
Contributor Author

jacg commented Aug 17, 2022

I'm trying to write an exhaustive set of simple-to-understand tests, covering all the things that should work. When I have all of these written out by hand, I'll try to write macros to generate them. These should have much in common, structurally, with the macros impl_ops macro that is to be used to generate the implementations. This should give me better insight into how impl_ops will need to be implemented, and some practice in actually implementing them, in a simpler context. So I'll come back to your macrology suggestions, once I've done that.

For now

<Quantity> *= <float>

works, but

<Quantity> *= <Ratio>

fails to compile. [And similarly for /=.]

Is the latter not supposed to work, or am I making a mistake somewhere?

@iliekturtles
Copy link
Owner

Good catch. MulAssign/DivAssign are currently only implemented for the underlying storage type.

uom/src/system.rs

Lines 542 to 554 in 10d9679

impl<D, U, V> $crate::lib::ops::$MulDivAssignTrait<V> for Quantity<D, U, V>
where
D: Dimension + ?Sized,
D::Kind: $crate::marker::$MulDivAssignTrait,
U: Units<V> + ?Sized,
V: $crate::num::Num + $crate::Conversion<V>
+ $crate::lib::ops::$MulDivAssignTrait<V>,
{
#[inline(always)]
fn $muldivassign_fun(&mut self, rhs: V) {
self.value $muldivassign_op rhs;
}
}

I don't see why it couldn't explicitly be implemented for Ratio (Quantity<DN<Z0>, ...>). It's possible there are trait bound issues, but I don't remember if I even tried to do the implementation.

impl<D, Ul, Ur, V> $crate::lib::ops::$MulDivAssignTrait<Quantity<DN<Z0>, Ur, V> for Quantity<D, Ul, V>
where ...
{
    ...
}

uom/src/system.rs

Lines 274 to 276 in 10d9679

// Type alias for dimensions where all exponents of the factors are the given value.
type DN<N> = dyn Dimension<$($symbol = system!(@replace $symbol N),)+
Kind = dyn $crate::Kind>;

@jacg
Copy link
Contributor Author

jacg commented Aug 17, 2022

I've pushed a commit which adds some very concise tests, whose purpose is to summarize what operations currently work.

Nothing fancy, no exhaustive property-based testing, just a single hand-picked example for each case, and some macros to maximize the signal-to-noise ratio in the test collection, so that it can act as a quick overview for a human reader. The layout is aggressively aligned to make parsing and comparison by human eye as easy as possible, so it will make any rustfmt checks have a fit, but those are irrelevant for the time being.

Could you glance over

uom/src/tests/impl_ops.rs

Lines 95 to 124 in 4de23e8

// The number in the comment after each test indicates which implementation
// provides the functionality being tested, according to the table shown here:
// https://github.com/iliekturtles/uom/pull/307#issuecomment-1186208970
mod vv {
mod autoconvert_no {
use super::super::concise::f32::*;
test!(add_conv m(1000.) + km(2.), m(3000.)); // 1
test!(sub_conv m(8000.) - km(2.), m(6000.)); // 1
test!(add m( 1.) + m(2.), m(3.)); // 2
test!(sub m( 8.) - m(2.), m(6.)); // 2
test!(add_assign_conv [ m(1000.) ] += km(2.), km(3.)); // 3
test!(sub_assign_conv [ m(8000.) ] -= km(2.), km(6.)); // 3
test!(add_assign [ m( 1.) ] += m(2.), m(3.)); // 4
test!(sub_assign [ m( 8.) ] -= m(2.), m(6.)); // 4
test!(mul_conv m(4000.) * km(2.), km2(8.)); // 5
test!(div_conv m(6000.) / km(2.), ratio(3.)); // 5
test!(mul m(4.) * m(2.), m2(8.)); // 6
test!(div m(6.) / s(2.), m_per_s(3.)); // 6
test!(mul_ratio_left ratio(4.) * m(2.), m(8.)); // 6
test!(div_ratio_left ratio(6.) / s(2.), hz(3.)); // 6
test!(mul_ratio_right m(4.) * ratio(2.), m(8.)); // 6 c.f. ABDC below
test!(div_ratio_right m(6.) / ratio(2.), m(3.)); // 6 c.f. WXYZ below
// test!(mul_assign_ratio [ m(4,) ] *= ratio(2.), m(8.)); // ERR c.f. ABCD above
// test!(div_assign_ratio [ m(6.) ] /= ratio(2.), m(3.)); // ERR c.f. WXYZ above
test!(mul_bare_right m(4.) * 2. , m(8.)); // 7
test!(div_bare_right m(6.) / 2. , m(3.)); // 7
test!(mul_assign_bare [ m(2.) ] *= 3. , m(6.)); // 8
test!(div_assign_bare [ m(6.) ] /= 3. , m(2.)); // 8
test!(mul_bare_left 4. * m(2.), m(8.)); // 9
test!(div_bare_left 6. / s(2.), hz(3.)); // 9

to check that I have tested everything that you expect to work at present, that the test names correctly reflect what is being tested, and that the numbers in the comments at the end of each line match the relevant implementations listed in #307 (comment).

As noted earlier, the assignment operators with Ratio on the RHS do not work, so those test are present, but commented out.

Would a small PR for the currently missing MulDivAssign Ratio case (if it can be made to work, haven't looked into it yet) make sense, before tackling the reference operand implementations in this PR?

@jacg jacg force-pushed the quantity-reference-ops branch 3 times, most recently from 286c1ee to f5239ce Compare August 17, 2022 17:14
@jacg
Copy link
Contributor Author

jacg commented Aug 18, 2022

I have added (in the latest commit, at present)

impl<D, Ul, Ur, V> $crate::lib::ops::$MulDivAssignTrait<Quantity<DimensionOne, Ur, V>> for Quantity<D, Ul, V>
where ...

but the relevant tests continue to fail to compile with exactly the same error message as before:

126 |         test!(mul_assign_ratio    [    m(4.) ] *= ratio(2.),        m(8.)); //  ERR      c.f. AAA above
    |                                                   ^^^^^^^^^ expected `f32`, found struct `si::Quantity`

Any idea why it's not picking up this new implementation as appropriate for the requested operation?

@iliekturtles
Copy link
Owner

A separate PR to add MulDivAssign for Ratio is a good idea.

Took me a while of staring at the code, but I believe the following two changes should get things compiling.

8c8
<                         + $crate::lib::ops::$MulDivAssignTrait<Quantity<DimensionOne, Ur, V>>,
---
>                         + $crate::lib::ops::$MulDivAssignTrait<V>,
12c12,13
<                         self.value $muldivassign_op rhs;
---
>                         self.value $muldivassign_op rhs.value;
>                         // change_base is needed for autoconvert! version.

@jacg
Copy link
Contributor Author

jacg commented Aug 30, 2022

A separate PR to add MulDivAssign for Ratio is a good idea.

Done in #364.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement operations for Quantity references
2 participants