Allow for passing decimals to Price mathematical operations

Created on 9 December 2024, 12 days ago

Describe your bug or feature request.

The mathematical helper functions on the \Drupal\commerce_price\Price class do not allow for the selection of decimals, sometimes resulting in an incorrect number of decimals for the given currency code.

Example: I have a commerce_price field on an entity. This field is populated dynamically on entity pre-save:

$total = new Price('100', 'USD');
$deposit_percentage = '12.125';
$deposit = $total->multiply($deposit_percentage)->divide('100');
// Deposit is 12.125.
$entity->set('field_deposit', $deposit);

The problem is that even though $total is set to USD (two decimals), after performing the mathematical operation, the $deposit, though still in USD, is 3 decimals. The resultant 3-decimal Price is then stored in the entity field, and on display, shows three decimals, even though the field is storing a price in USD, which should only show two decimals.

I see that, for example, \Drupal\commerce_price\Price::divide(string $number) : Price; does not allow for passing the scale (number of decimals), but that the underlying calculator does allow passing the scale: \Drupal\commerce_price\Calculator::devide(string $first_number, string $second_number, int $scale = 6) : string.

I'm thinking that Price::[MATHEMATICAL OPERATION] helpers should also accept the $scale argument, and pass it to the calculator, removing the necessity of having to call the calculator directly to set the decimal precision. For example Price::divide:

public function divide(string $number, $scale = 6) : Price {
  $new_number = Calculator::divide($this->number, $number, $scale);
  return new static($new_number, $this->currencyCode);
}
Feature request
Status

Active

Version

3.0

Component

Developer experience

Created by

🇨🇦Canada Jaypan

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @Jaypan
  • 🇮🇱Israel jsacksick

    Ok, sure why not, let's add an optional scale parameter then.

  • 🇷🇸Serbia bojanz

    Setting $scale will truncate any extra decimals. In an eCommerce context this is rarely what you want, if you just multiplied/divided something, it would be more precise to round instead. That's why $scale wasn't exposed when we wrote the API, we wanted to guide people towards rounding.

  • 🇮🇱Israel jsacksick

    Won't fix it is! Thanks Bojan!

  • 🇨🇦Canada Jaypan

    Thanks, I didn't know round was the better option. It would be nice to have a Price::round() helper method then. Is there any reason to not add that to Price? I'll put together a PR if there is no reason to not have it.

  • 🇷🇸Serbia bojanz

    There is a Rounder service. $rounder->round($price) takes the configured number of decimals for a currency and rounds to that precision.
    This loading of the number of decimals is why it's a separate service.

    If you know the number of decimals yourself, you can always call Calculator::round() directly.

  • Pipeline finished with Success
    10 days ago
    Total: 705s
    #366753
  • 🇮🇱Israel jsacksick

    The \Drupal calls are causing phpstan errors... What Bojan was suggesting was to invoke the Calculator directly.
    The only problem is that you'd need access to the currency fraction digits, and that requires loading the currency... Which when done properly requires loading the currency from the currency storage (and that again will require adding \Drupal:: calls which isn't clean...

    So that lives us with this:

      public function round($mode = PHP_ROUND_HALF_UP) {
         $currency = Currency::load($price->getCurrencyCode());
         $rounded_number = Calculator::round($this->getNumber(), $currency->getFractionDigits(), $mode);
         return $rounded_number;
      }
    

    Loading the currency like the following:
    $currency = Currency::load($price->getCurrencyCode());

    isn't best practice...

  • 🇷🇸Serbia bojanz

    If a $price->round() is desired, perhaps it take a $digits argument, and then you use Rounder if you want $digits to be auto-resolved for you?

  • 🇨🇦Canada Jaypan

    It sounds like it's best to just use the calculator class to round. Thanks for the explanations!

Production build 0.71.5 2024