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

Redefine rescale as rescale_mut and introduce an inline rescale function #391

Open
cksac opened this issue Jun 6, 2021 · 7 comments
Open
Milestone

Comments

@cksac
Copy link

cksac commented Jun 6, 2021

Is below expected?

    let amount =
        (dec!(1000.00) * dec!(0.5) / dec!(2)).round_dp_with_strategy(2, RoundingStrategy::ToZero);
    println!("{}", amount);
    assert_eq!(2, amount.scale());

    let amount = (dec!(1000.00) * dec!(0.5) / dec!(2.00))
        .round_dp_with_strategy(2, RoundingStrategy::ToZero);
    println!("{}", amount);
    assert_eq!(2, amount.scale());

output

250.00
250.0
@paupino
Copy link
Owner

paupino commented Jun 6, 2021

Hi @cksac ,

Thank you for the question: short answer is yes, it is expected behavior at the moment. I'll try to explain:

For the first example (1000.00 * 0.5 / 2), the multiply expands the scale by one so: 1000.00 * 0.5 = 500.000. The reason for this is that multiply adds together scales (e.g. 2 + 1). The division, in this case doesn't change the scale so we get 500.000 / 2 = 250.000.
When we do the round, we're effectively reducing precision. In this case rounding 250.000 to 2 decimal places gives us 250.00. What I mean by reducing precision is that in this case 250.000 is more precise than 250.00 - it's a little bit hypothetical in this situation however if we had 333.333 instead then it makes a bit more sense.

In the second example (1000.00 * 0.5 / 2.00) we get the same result for the multiply. The division, however is able to reduce the scale by 2 since we're able to assert that level of precision. So the outcome of 500.000 / 2.00 is 250.0. Now when we go to round the number 250.0, we don't need to reduce scale any further so we can return that number as is.

I wanted to check other libraries and see that the logic behind this closely mirrors .NET decimal behavior (e.g. https://dotnetfiddle.net/QY5ub3). That doesn't mean all that much except that we're being consistent in handling behavior like this.

I do think that it could be considered confusing if you're expecting round to increase scale (i.e. 250.0 rounded to 250.00). If you need this logic then I'd actually recommend multiplying by 1.00 before rounding to force to 2dp. The other option is to introduce another rounding function to guarantee the scale after calling perhaps?

Regardless, I'm going to keep this issue open. I do think this warrants some further documentation to clearly explain behavior such as this as well as suggested "workarounds" (in lieu of explicit functions).

@cksac
Copy link
Author

cksac commented Jun 7, 2021

Thanks for detail explanation. I think introduce another rounding function to guarantee the scale would be nice.

@paupino
Copy link
Owner

paupino commented Jun 7, 2021

Hi @cksac - just thinking about it... would rescale suffice here?
Also, if this is purely for display purposes there are some format modifiers that could be useful.
Regardless, hopefully you find a solution in the meantime!

@cksac
Copy link
Author

cksac commented Jun 10, 2021

Hi @paupino, thanks for suggestion. rescale works, but I can't chain it after round_dp_with_strategy. Therefore, I create an extension trait instead.

impl DecimalExt for Decimal {
    fn truncate_p(&self, precision: u32) -> Self {
        let mut p = self.round_dp_with_strategy(precision, RoundingStrategy::ToZero);
        p.rescale(precision);
        p
    }

    fn floor_p(&self, precision: u32) -> Self {
        let mut p = self.round_dp_with_strategy(precision, RoundingStrategy::ToNegativeInfinity);
        p.rescale(precision);
        p
    }

    fn ceil_p(&self, precision: u32) -> Self {
        let mut p = self.round_dp_with_strategy(precision, RoundingStrategy::ToPositiveInfinity);
        p.rescale(precision);
        p
    }
}

@paupino paupino added this to the v1.16 milestone Jul 28, 2021
@paupino
Copy link
Owner

paupino commented Sep 14, 2021

I think the action for this ticket is to implement an inline rescale. The typical standard seems to be to do rescale and rescale_mut - which of course is a breaking change in the existing API (e.g. even with must_use it would break downstream code silently).
I've got two options here really:

  1. Introduce a nextgen feature and start cleaning up API's in advance.
  2. Wait until v2 to repurpose the feature.

My concern with the nextgen feature is that it could make the API confusing and the docs hard to read. For now, I'm going to re-title this issue and mark it for a v2 feature.

@paupino paupino removed this from the v1.16 milestone Sep 14, 2021
@paupino paupino changed the title Inconsistent scale return by round_dp_with_strategy Redefine rescale as rescale_mut and introduce an inline rescale function Sep 14, 2021
@paupino paupino added this to the v2.0 milestone Sep 14, 2021
@schungx
Copy link
Contributor

schungx commented Sep 15, 2021

This only ever happens when printing the actual result, and Rust has different formatting rules on displaying arbitrary-length decimal digits (i.e. stopping at the last infinite stream of zeros) vs. displaying specified-precision decimal digits (i.e. always displays the same number of digits).

In Rust, if no specific formatting flag is specified, the standard display style is arbitrary-length format, so I suggest we should just mirror that.

If you think about it, the integral part of the number also does not, by default, have leading zeros to indicate precision (but there is also a formatting flag to add the zeros). So it is consistent.

@korrat
Copy link

korrat commented Sep 28, 2021

Until this is implemented, Tap::tap_mut from the tap crate can be used as a workaround.

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

No branches or pull requests

5 participants