diff --git a/doc/features/operation.rst b/doc/features/operation.rst index e8c4ffc0..a63ff05b 100644 --- a/doc/features/operation.rst +++ b/doc/features/operation.rst @@ -47,6 +47,20 @@ Divisions can be performed using ``divide()``. $result = $value->divide(2); // €4.00 +.. _modulus: + +Modulus +------- + +Modulus operations can be performed using ``mod()``. + +.. code-block:: php + + $value = Money::EUR(830); // €8.30 + $divisor = Money::EUR(300); // €3.00 + + $result = $value->mod($divisor); // €2.30 + .. _rounding_modes: Rounding Modes diff --git a/spec/Calculator/CalculatorBehavior.php b/spec/Calculator/CalculatorBehavior.php index ec4c46e7..25456bdc 100644 --- a/spec/Calculator/CalculatorBehavior.php +++ b/spec/Calculator/CalculatorBehavior.php @@ -72,6 +72,11 @@ function it_shares_a_value() $this->share(10, 2, 4)->shouldBeString(); } + function it_calculates_the_modulus() + { + $this->mod(11, 5)->shouldBeString(); + } + public function getMatchers() { return [ diff --git a/spec/MoneySpec.php b/spec/MoneySpec.php index 1ff7699e..b5ee08c1 100644 --- a/spec/MoneySpec.php +++ b/spec/MoneySpec.php @@ -320,6 +320,23 @@ function it_calculates_the_absolute_amount(Calculator $calculator) $money->getAmount()->shouldBeLike(1); } + function it_calculates_a_modulus_with_an_other_money(Calculator $calculator) + { + $result = self::AMOUNT % self::OTHER_AMOUNT; + $calculator->mod((string) self::AMOUNT, (string) self::OTHER_AMOUNT)->willReturn((string) $result); + $money = $this->mod(new Money(self::OTHER_AMOUNT, new Currency(self::CURRENCY))); + + $money->shouldHaveType(Money::class); + $money->getAmount()->shouldBe((string) $result); + } + + function it_throws_an_exception_when_currency_is_different_during_modulus(Calculator $calculator) + { + $calculator->mod((string) self::AMOUNT, (string) self::AMOUNT)->shouldNotBeCalled(); + + $this->shouldThrow(\InvalidArgumentException::class)->duringMod(new Money(self::AMOUNT, new Currency(self::OTHER_CURRENCY))); + } + public function getMatchers() { return [ diff --git a/src/Calculator.php b/src/Calculator.php index 37b19445..7e52bce4 100644 --- a/src/Calculator.php +++ b/src/Calculator.php @@ -114,4 +114,14 @@ public function round($number, $roundingMode); * @return string */ public function share($amount, $ratio, $total); + + /** + * Get the modulus of an amount. + * + * @param string $amount + * @param int|float|string $divisor + * + * @return string + */ + public function mod($amount, $divisor); } diff --git a/src/Calculator/BcMathCalculator.php b/src/Calculator/BcMathCalculator.php index fa7382a6..9dbbf0da 100644 --- a/src/Calculator/BcMathCalculator.php +++ b/src/Calculator/BcMathCalculator.php @@ -228,4 +228,12 @@ public function share($amount, $ratio, $total) { return $this->floor(bcdiv(bcmul($amount, $ratio, $this->scale), $total, $this->scale)); } + + /** + * {@inheritdoc} + */ + public function mod($amount, $divisor) + { + return bcmod($amount, $divisor); + } } diff --git a/src/Calculator/GmpCalculator.php b/src/Calculator/GmpCalculator.php index 112717ed..d92e3f16 100644 --- a/src/Calculator/GmpCalculator.php +++ b/src/Calculator/GmpCalculator.php @@ -286,4 +286,22 @@ public function share($amount, $ratio, $total) { return $this->floor($this->divide($this->multiply($amount, $ratio), $total)); } + + /** + * {@inheritdoc} + */ + public function mod($amount, $divisor) + { + // gmp_mod() only calculates non-negative integers, so we use absolutes + $remainder = gmp_mod($this->absolute($amount), $this->absolute($divisor)); + + // If the amount was negative, we negate the result of the modulus operation + $amount = Number::fromString((string) $amount); + + if (true === $amount->isNegative()) { + $remainder = gmp_neg($remainder); + } + + return gmp_strval($remainder); + } } diff --git a/src/Calculator/PhpCalculator.php b/src/Calculator/PhpCalculator.php index b9e35c96..acd6bc2e 100644 --- a/src/Calculator/PhpCalculator.php +++ b/src/Calculator/PhpCalculator.php @@ -139,6 +139,18 @@ public function share($amount, $ratio, $total) return $this->castInteger(floor($amount * $ratio / $total)); } + /** + * {@inheritdoc} + */ + public function mod($amount, $divisor) + { + $result = $amount % $divisor; + + $this->assertIntegerBounds($result); + + return (string) $result; + } + /** * Asserts that an integer value didn't become something else * (after some arithmetic operation). diff --git a/src/Money.php b/src/Money.php index 2f64140f..617bfb21 100644 --- a/src/Money.php +++ b/src/Money.php @@ -342,6 +342,22 @@ public function divide($divisor, $roundingMode = self::ROUND_HALF_UP) return $this->newInstance($quotient); } + /** + * Returns a new Money object that represents + * the remainder after dividing the value by + * the given factor. + * + * @param Money $divisor + * + * @return Money + */ + public function mod(Money $divisor) + { + $this->assertSameCurrency($divisor); + + return new self($this->getCalculator()->mod($this->amount, $divisor->amount), $this->currency); + } + /** * Allocate the money according to a list of ratios. * diff --git a/tests/Calculator/CalculatorTestCase.php b/tests/Calculator/CalculatorTestCase.php index 190ee8c2..4a3ba8c8 100644 --- a/tests/Calculator/CalculatorTestCase.php +++ b/tests/Calculator/CalculatorTestCase.php @@ -105,6 +105,15 @@ public function it_compares_values($left, $right, $expected) $this->assertEquals($expected, $this->getCalculator()->compare($left, $right)); } + /** + * @dataProvider modExamples + * @test + */ + public function it_calculates_the_modulus_of_a_value($left, $right, $expected) + { + $this->assertEquals($expected, $this->getCalculator()->mod($left, $right)); + } + public function additionExamples() { return [ @@ -198,4 +207,17 @@ public function compareExamples() ['1', '0.000000000000000000000000005', 1], ]; } + + public function modExamples() + { + return [ + [11, 5, '1'], + [9, 3, '0'], + [1006, 10, '6'], + [1007, 10, '7'], + [-13, -5, '-3'], + [-13, 5, '-3'], + [13, -5, '3'], + ]; + } } diff --git a/tests/MoneyTest.php b/tests/MoneyTest.php index a485db83..f9f66f08 100644 --- a/tests/MoneyTest.php +++ b/tests/MoneyTest.php @@ -190,6 +190,21 @@ public function it_calculates_the_negative_amount($amount, $result) $this->assertEquals($result, $money->getAmount()); } + /** + * @dataProvider modExamples + * @test + */ + public function it_calculates_the_modulus_of_an_amount($left, $right, $expected) + { + $money = new Money($left, new Currency(self::CURRENCY)); + $rightMoney = new Money($right, new Currency(self::CURRENCY)); + + $money = $money->mod($rightMoney); + + $this->assertInstanceOf(Money::class, $money); + $this->assertEquals($expected, $money->getAmount()); + } + public function test_it_converts_to_json() { $this->assertEquals( @@ -296,4 +311,14 @@ public function negativeExamples() ['-1', 1], ]; } + + public function modExamples() + { + return [ + [11, 5, '1'], + [9, 3, '0'], + [1006, 10, '6'], + [1007, 10, '7'], + ]; + } }